rakit 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/rakit +105 -7
- data/lib/generated/azure.devops_pb.rb +1 -1
- data/lib/generated/example_pb.rb +1 -1
- data/lib/generated/rakit.azure_pb.rb +26 -0
- data/lib/generated/rakit.docfx_pb.rb +18 -0
- data/lib/generated/rakit.example_pb.rb +18 -0
- data/lib/generated/rakit.markdown_pb.rb +19 -0
- data/lib/generated/rakit.shell_pb.rb +22 -0
- data/lib/generated/rakit.static_web_server_pb.rb +20 -0
- data/lib/rakit/azure/dev_ops.rb +1 -1
- data/lib/rakit/docfx.rb +72 -0
- data/lib/rakit/gem.rb +19 -0
- data/lib/rakit/hugo.rb +43 -0
- data/lib/rakit/markdown.rb +274 -82
- data/lib/rakit/shell.rb +1 -1
- data/lib/rakit/static_web_server.rb +90 -24
- data/lib/rakit.rb +3 -0
- metadata +9 -2
- data/lib/rakit/cli/markdown.rb +0 -90
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1df32ab5eaf41ff5b000ebeb8893a137b25b9d99d2620e528f67c973486eb412
|
|
4
|
+
data.tar.gz: aac1f2243e54107dbb1686fba35d2e7e3a53064272a91ed07c4fbc1ca9818da1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef24f803a1e7361483201238adedf0d6bcd425b3f355774ba1e560c940b8bb531fa0170cf3fc7be6b71204b1f281b5a3812e39dd4c4cc6fd6313b688bb456705
|
|
7
|
+
data.tar.gz: 0af3ed799d213fe132a9e7b516374b9ac64f4ea7695140ebb21c091d6e14b04062664e5ae96f2618ba2837ce15351e2e3ba86a4f07bf710daf7469370cc321ea
|
data/exe/rakit
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# CLI for rakit gem. Subcommands: markdown (
|
|
5
|
-
# CLI for rakit gem. Subcommands: file (list|copy), word-count (--json-keys), static-web-server (start|stop|running|publish), word-cloud (status|install|generate).
|
|
4
|
+
# CLI for rakit gem. Subcommands: markdown (amalgamate|disaggregate|validate), file (list|copy), word-count, word-cloud, static-web-server (start|stop|running|publish|view).
|
|
6
5
|
|
|
7
6
|
require "rakit"
|
|
8
7
|
|
|
@@ -17,18 +16,30 @@ def usage_stderr(msg = nil)
|
|
|
17
16
|
$stderr.puts " file list <directory> [--recursive] [--include-hidden] [--format console|json|proto-json]"
|
|
18
17
|
$stderr.puts " file copy <source> <destination> [--overwrite] [--create-directories] [--format ...]"
|
|
19
18
|
$stderr.puts " word-count <file>|--stdin --json-keys [--format console|json|proto-json] [options]"
|
|
20
|
-
$stderr.puts "
|
|
19
|
+
$stderr.puts " markdown amalgamate <root_dir> --out <output_path>"
|
|
20
|
+
$stderr.puts " markdown disaggregate <markdown_path> --out <output_root_dir>"
|
|
21
|
+
$stderr.puts " markdown validate <root_dir>"
|
|
22
|
+
$stderr.puts " static-web-server <start|stop|running|publish|view> [options] [args]"
|
|
21
23
|
$stderr.puts " word-cloud <status|install|generate> [options] [args]"
|
|
24
|
+
$stderr.puts " start [--port PORT] Start server (idempotent)"
|
|
25
|
+
$stderr.puts " stop Stop server"
|
|
26
|
+
$stderr.puts " running Exit 0 if running, non-zero otherwise"
|
|
27
|
+
$stderr.puts " publish <site_name> <source_dir> Publish static site"
|
|
28
|
+
$stderr.puts " view [path] Open path in browser (default /louparslow/rakit/index.html)"
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
def main(argv = ARGV)
|
|
25
32
|
return usage_stderr("Expected subcommand.") if argv.empty?
|
|
26
33
|
|
|
27
34
|
sub = argv[0]
|
|
28
|
-
if sub == "
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
35
|
+
if sub == "--version" || sub == "-v"
|
|
36
|
+
spec = Gem.loaded_specs["rakit"]
|
|
37
|
+
puts spec ? "#{spec.name} #{spec.version}" : "rakit (version unknown)"
|
|
38
|
+
return 0
|
|
39
|
+
end
|
|
40
|
+
if sub == "--help" || sub == "-h"
|
|
41
|
+
usage_stderr
|
|
42
|
+
return 0
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
if sub == "file"
|
|
@@ -43,6 +54,82 @@ def main(argv = ARGV)
|
|
|
43
54
|
return Rakit::CLI::WordCount.run(argv)
|
|
44
55
|
end
|
|
45
56
|
|
|
57
|
+
if sub == "markdown"
|
|
58
|
+
argv.shift
|
|
59
|
+
cmd = argv.shift
|
|
60
|
+
begin
|
|
61
|
+
case cmd
|
|
62
|
+
when "amalgamate"
|
|
63
|
+
root_dir = argv.shift
|
|
64
|
+
out_idx = argv.index("--out")
|
|
65
|
+
output_path = out_idx && argv[out_idx + 1] ? argv[out_idx + 1] : nil
|
|
66
|
+
unless root_dir && output_path
|
|
67
|
+
usage_stderr("markdown amalgamate requires <root_dir> and --out <output_path>")
|
|
68
|
+
return 1
|
|
69
|
+
end
|
|
70
|
+
doc = Rakit::Markdown.load_document(root_dir: root_dir)
|
|
71
|
+
Rakit::Markdown.amalgamate(document: doc, output_path: output_path)
|
|
72
|
+
return 0
|
|
73
|
+
when "disaggregate"
|
|
74
|
+
md_path = argv.shift
|
|
75
|
+
out_idx = argv.index("--out")
|
|
76
|
+
output_root = out_idx && argv[out_idx + 1] ? argv[out_idx + 1] : nil
|
|
77
|
+
unless md_path && output_root
|
|
78
|
+
usage_stderr("markdown disaggregate requires <markdown_path> and --out <output_root_dir>")
|
|
79
|
+
return 1
|
|
80
|
+
end
|
|
81
|
+
Rakit::Markdown.disaggregate(markdown_path: md_path, output_root_dir: output_root)
|
|
82
|
+
return 0
|
|
83
|
+
when "validate"
|
|
84
|
+
root_dir = argv.shift
|
|
85
|
+
unless root_dir
|
|
86
|
+
usage_stderr("markdown validate requires <root_dir>")
|
|
87
|
+
return 1
|
|
88
|
+
end
|
|
89
|
+
if Rakit::Markdown.validate(root_dir: root_dir)
|
|
90
|
+
return 0
|
|
91
|
+
else
|
|
92
|
+
Rakit::Markdown.validation_failures.each { |msg| $stderr.puts msg }
|
|
93
|
+
return 1
|
|
94
|
+
end
|
|
95
|
+
when "merge"
|
|
96
|
+
source_dir = argv.shift
|
|
97
|
+
target_path = argv.shift
|
|
98
|
+
create_dirs = argv.include?("--create-directories")
|
|
99
|
+
unless source_dir && target_path
|
|
100
|
+
usage_stderr("markdown merge requires <source_dir> and <target_path>")
|
|
101
|
+
return 1
|
|
102
|
+
end
|
|
103
|
+
result = Rakit::Markdown.merge(source_dir, target_path, create_directories: create_dirs)
|
|
104
|
+
unless result[:success]
|
|
105
|
+
$stderr.puts result[:message] if result[:message].to_s.length > 0
|
|
106
|
+
return 1
|
|
107
|
+
end
|
|
108
|
+
return 0
|
|
109
|
+
when "pdf"
|
|
110
|
+
input_md = argv.shift
|
|
111
|
+
output_pdf = argv.shift
|
|
112
|
+
unless input_md
|
|
113
|
+
usage_stderr("markdown pdf requires <input.md>")
|
|
114
|
+
return 1
|
|
115
|
+
end
|
|
116
|
+
result = Rakit::Markdown.generate_pdf(input_md, output_pdf)
|
|
117
|
+
unless result[:success]
|
|
118
|
+
$stderr.puts result[:message] if result[:message].to_s.length > 0
|
|
119
|
+
$stderr.puts result[:stderr] if result[:stderr].to_s.length > 0
|
|
120
|
+
return 1
|
|
121
|
+
end
|
|
122
|
+
return 0
|
|
123
|
+
else
|
|
124
|
+
usage_stderr(cmd ? "markdown: use amalgamate, disaggregate, validate, merge, or pdf" : "markdown requires a command")
|
|
125
|
+
return 1
|
|
126
|
+
end
|
|
127
|
+
rescue ArgumentError, Errno::ENOENT, Errno::EACCES => e
|
|
128
|
+
$stderr.puts e.message
|
|
129
|
+
return 1
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
46
133
|
if sub == "word-cloud"
|
|
47
134
|
argv.shift
|
|
48
135
|
require "rakit/cli/word_cloud"
|
|
@@ -65,6 +152,7 @@ def main(argv = ARGV)
|
|
|
65
152
|
end
|
|
66
153
|
end
|
|
67
154
|
Rakit::StaticWebServer.start(port: port)
|
|
155
|
+
puts "http://localhost:#{port}"
|
|
68
156
|
return 0
|
|
69
157
|
when "stop"
|
|
70
158
|
Rakit::StaticWebServer.stop
|
|
@@ -80,6 +168,16 @@ def main(argv = ARGV)
|
|
|
80
168
|
end
|
|
81
169
|
Rakit::StaticWebServer.publish(site_name, source_dir)
|
|
82
170
|
return 0
|
|
171
|
+
when "view"
|
|
172
|
+
# Default to project Hugo docs index (same as rake view_docs)
|
|
173
|
+
path = argv.shift || "/louparslow/rakit/index.html"
|
|
174
|
+
begin
|
|
175
|
+
Rakit::StaticWebServer.view(path)
|
|
176
|
+
return 0
|
|
177
|
+
rescue RuntimeError => e
|
|
178
|
+
$stderr.puts e.message
|
|
179
|
+
return 1
|
|
180
|
+
end
|
|
83
181
|
else
|
|
84
182
|
usage_stderr(cmd ? "Unknown command: #{cmd}" : "Expected command after static-web-server.")
|
|
85
183
|
return 1
|
|
@@ -7,7 +7,7 @@ require 'google/protobuf'
|
|
|
7
7
|
|
|
8
8
|
descriptor_data = "\n\x12\x61zure.devops.proto\x12\x0brakit.azure\"L\n\x08Pipeline\x12\x0b\n\x03org\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0bpipeline_id\x18\x03 \x01(\x05\x12\r\n\x05token\x18\x04 \x01(\t\"R\n\x18GetPipelineResultRequest\x12\'\n\x08pipeline\x18\x01 \x01(\x0b\x32\x15.rakit.azure.Pipeline\x12\r\n\x05token\x18\x02 \x01(\t\"C\n\x0ePipelineStatus\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x03(\t\x12\x10\n\x08warnings\x18\x03 \x03(\t\"8\n\x0ePipelineResult\x12&\n\x04runs\x18\x01 \x03(\x0b\x32\x18.rakit.azure.PipelineRun\"\x97\x01\n\x0bPipelineRun\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\t\x12\x0e\n\x06result\x18\x04 \x01(\t\x12\x14\n\x0c\x63reated_date\x18\x05 \x01(\t\x12\x15\n\rfinished_date\x18\x06 \x01(\t\x12\"\n\x06stages\x18\x07 \x03(\x0b\x32\x12.rakit.azure.Stage\"&\n\x05Issue\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\"G\n\x03Job\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\"\n\x06issues\x18\x03 \x03(\x0b\x32\x12.rakit.azure.Issue\"i\n\x05Stage\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\x1e\n\x04jobs\x18\x03 \x03(\x0b\x32\x10.rakit.azure.Job\x12\"\n\x06issues\x18\x04 \x03(\x0b\x32\x12.rakit.azure.Issue\"`\n\x0eTimelineRecord\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0e\n\x06result\x18\x03 \x01(\t\x12\"\n\x06issues\x18\x04 \x03(\x0b\x32\x12.rakit.azure.Issue\"\xa8\x01\n\x14PipelineResultDetail\x12\x12\n\nsuccessful\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x01(\t\x12\x10\n\x08warnings\x18\x03 \x01(\t\x12%\n\x03run\x18\x04 \x01(\x0b\x32\x18.rakit.azure.PipelineRun\x12\x33\n\x0e\x66\x61iled_records\x18\x05 \x03(\x0b\x32\x1b.rakit.azure.TimelineRecord2i\n\x0ePipelineServer\x12W\n\x11GetPipelineResult\x12%.rakit.azure.GetPipelineResultRequest\x1a\x1b.rakit.azure.PipelineResultB\x0f\xea\x02\x0cRakit::Azureb\x06proto3"
|
|
9
9
|
|
|
10
|
-
pool =
|
|
10
|
+
pool = Google::Protobuf::DescriptorPool.generated_pool
|
|
11
11
|
pool.add_serialized_file(descriptor_data)
|
|
12
12
|
|
|
13
13
|
module Rakit
|
data/lib/generated/example_pb.rb
CHANGED
|
@@ -7,7 +7,7 @@ require 'google/protobuf'
|
|
|
7
7
|
|
|
8
8
|
descriptor_data = "\n\rexample.proto\x12\rrakit.example\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\" \n\rHelloResponse\x12\x0f\n\x07message\x18\x01 \x01(\tB\x13\xea\x02\x10Rakit::Generatedb\x06proto3"
|
|
9
9
|
|
|
10
|
-
pool =
|
|
10
|
+
pool = Google::Protobuf::DescriptorPool.generated_pool
|
|
11
11
|
pool.add_serialized_file(descriptor_data)
|
|
12
12
|
|
|
13
13
|
module Rakit
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: rakit.azure.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x11rakit.azure.proto\x12\x0brakit.azure\"L\n\x08Pipeline\x12\x0b\n\x03org\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0bpipeline_id\x18\x03 \x01(\x05\x12\r\n\x05token\x18\x04 \x01(\t\"R\n\x18GetPipelineResultRequest\x12\'\n\x08pipeline\x18\x01 \x01(\x0b\x32\x15.rakit.azure.Pipeline\x12\r\n\x05token\x18\x02 \x01(\t\"C\n\x0ePipelineStatus\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x03(\t\x12\x10\n\x08warnings\x18\x03 \x03(\t\"8\n\x0ePipelineResult\x12&\n\x04runs\x18\x01 \x03(\x0b\x32\x18.rakit.azure.PipelineRun\"\x97\x01\n\x0bPipelineRun\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\t\x12\x0e\n\x06result\x18\x04 \x01(\t\x12\x14\n\x0c\x63reated_date\x18\x05 \x01(\t\x12\x15\n\rfinished_date\x18\x06 \x01(\t\x12\"\n\x06stages\x18\x07 \x03(\x0b\x32\x12.rakit.azure.Stage\"&\n\x05Issue\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\"G\n\x03Job\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\"\n\x06issues\x18\x03 \x03(\x0b\x32\x12.rakit.azure.Issue\"i\n\x05Stage\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\x1e\n\x04jobs\x18\x03 \x03(\x0b\x32\x10.rakit.azure.Job\x12\"\n\x06issues\x18\x04 \x03(\x0b\x32\x12.rakit.azure.Issue\"`\n\x0eTimelineRecord\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0e\n\x06result\x18\x03 \x01(\t\x12\"\n\x06issues\x18\x04 \x03(\x0b\x32\x12.rakit.azure.Issue\"\xa8\x01\n\x14PipelineResultDetail\x12\x12\n\nsuccessful\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x01(\t\x12\x10\n\x08warnings\x18\x03 \x01(\t\x12%\n\x03run\x18\x04 \x01(\x0b\x32\x18.rakit.azure.PipelineRun\x12\x33\n\x0e\x66\x61iled_records\x18\x05 \x03(\x0b\x32\x1b.rakit.azure.TimelineRecord2i\n\x0ePipelineServer\x12W\n\x11GetPipelineResult\x12%.rakit.azure.GetPipelineResultRequest\x1a\x1b.rakit.azure.PipelineResultB\x0f\xea\x02\x0cRakit::Azureb\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Rakit
|
|
14
|
+
module Azure
|
|
15
|
+
Pipeline = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Pipeline").msgclass
|
|
16
|
+
GetPipelineResultRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.GetPipelineResultRequest").msgclass
|
|
17
|
+
PipelineStatus = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineStatus").msgclass
|
|
18
|
+
PipelineResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineResult").msgclass
|
|
19
|
+
PipelineRun = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineRun").msgclass
|
|
20
|
+
Issue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Issue").msgclass
|
|
21
|
+
Job = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Job").msgclass
|
|
22
|
+
Stage = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.Stage").msgclass
|
|
23
|
+
TimelineRecord = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.TimelineRecord").msgclass
|
|
24
|
+
PipelineResultDetail = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.azure.PipelineResultDetail").msgclass
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: rakit.docfx.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x11rakit.docfx.proto\x12\x0brakit.docfx\"6\n\x0c\x42uildRequest\x12\x12\n\nsource_dir\x18\x01 \x01(\t\x12\x12\n\noutput_dir\x18\x02 \x01(\t\"/\n\x0b\x42uildResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\tB\x13\xea\x02\x10Rakit::Generatedb\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Rakit
|
|
14
|
+
module Generated
|
|
15
|
+
BuildRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.docfx.BuildRequest").msgclass
|
|
16
|
+
BuildResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.docfx.BuildResult").msgclass
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: rakit.example.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x13rakit.example.proto\x12\rrakit.example\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\" \n\rHelloResponse\x12\x0f\n\x07message\x18\x01 \x01(\tB\x13\xea\x02\x10Rakit::Generatedb\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Rakit
|
|
14
|
+
module Generated
|
|
15
|
+
HelloRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.example.HelloRequest").msgclass
|
|
16
|
+
HelloResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.example.HelloResponse").msgclass
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: rakit.markdown.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x14rakit.markdown.proto\x12\x0erakit.markdown\"r\n\x07Section\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0c\n\x04\x62ody\x18\x03 \x01(\t\x12\x13\n\x0bsource_path\x18\x04 \x01(\t\x12)\n\x08sections\x18\x05 \x03(\x0b\x32\x17.rakit.markdown.Section\"V\n\x08\x44ocument\x12\r\n\x05title\x18\x01 \x01(\t\x12\x10\n\x08root_dir\x18\x02 \x01(\t\x12)\n\x08sections\x18\x03 \x03(\x0b\x32\x17.rakit.markdown.Section\"3\n\x10ValidationResult\x12\r\n\x05valid\x18\x01 \x01(\x08\x12\x10\n\x08\x66\x61ilures\x18\x02 \x03(\tb\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Rakit
|
|
14
|
+
module Markdown
|
|
15
|
+
Section = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.markdown.Section").msgclass
|
|
16
|
+
Document = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.markdown.Document").msgclass
|
|
17
|
+
ValidationResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.markdown.ValidationResult").msgclass
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: rakit.shell.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x11rakit.shell.proto\x12\x0brakit.shell\"\x9a\x02\n\x07\x43ommand\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x02 \x03(\t\x12\x19\n\x11working_directory\x18\x03 \x01(\t\x12\x17\n\x0ftimeout_seconds\x18\x04 \x01(\x05\x12\x1a\n\x12\x65xpected_exit_code\x18\x05 \x01(\x05\x12\x17\n\x0f\x65xpected_stdout\x18\x06 \x01(\t\x12\x17\n\x0f\x65xpected_stderr\x18\x07 \x01(\t\x12<\n\x13\x61\x63\x63\x65ptance_criteria\x18\x08 \x03(\x0b\x32\x1f.rakit.shell.AcceptanceCriteria\x12\x13\n\x0b\x65xit_status\x18\t \x01(\x05\x12\x0e\n\x06stdout\x18\n \x01(\t\x12\x0e\n\x06stderr\x18\x0b \x01(\t\"1\n\x12\x41\x63\x63\x65ptanceCriteria\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"-\n\nTestResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0e\n\x06\x65rrors\x18\x02 \x03(\t\"b\n\rFormatRequest\x12%\n\x07\x63ommand\x18\x01 \x01(\x0b\x32\x14.rakit.shell.Command\x12*\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x1a.rakit.shell.CommandFormat\" \n\x0e\x46ormatResponse\x12\x0e\n\x06output\x18\x01 \x01(\t*Z\n\rCommandFormat\x12\x1e\n\x1a\x43OMMAND_FORMAT_UNSPECIFIED\x10\x00\x12\x0c\n\x08ONE_LINE\x10\x01\x12\x0e\n\nMULTI_LINE\x10\x02\x12\x0b\n\x07\x43OMPACT\x10\x03\x32\xc1\x01\n\x0e\x43ommandService\x12\x35\n\x07\x45xecute\x12\x14.rakit.shell.Command\x1a\x14.rakit.shell.Command\x12\x35\n\x04Test\x12\x14.rakit.shell.Command\x1a\x17.rakit.shell.TestResult\x12\x41\n\x06\x46ormat\x12\x1a.rakit.shell.FormatRequest\x1a\x1b.rakit.shell.FormatResponseB\x0f\xea\x02\x0cRakit::Shellb\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Rakit
|
|
14
|
+
module Shell
|
|
15
|
+
Command = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.Command").msgclass
|
|
16
|
+
AcceptanceCriteria = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.AcceptanceCriteria").msgclass
|
|
17
|
+
TestResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.TestResult").msgclass
|
|
18
|
+
FormatRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.FormatRequest").msgclass
|
|
19
|
+
FormatResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.FormatResponse").msgclass
|
|
20
|
+
CommandFormat = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.shell.CommandFormat").enummodule
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: rakit.static_web_server.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x1drakit.static_web_server.proto\x12\x17rakit.static_web_server\"T\n\x15StaticWebServerConfig\x12\x16\n\x0eroot_directory\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\r\x12\x15\n\rhttps_enabled\x18\x03 \x01(\x08\"=\n\x0ePublishRequest\x12\x11\n\tsite_name\x18\x01 \x01(\t\x12\x18\n\x10source_directory\x18\x02 \x01(\t\"1\n\rPublishResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\"-\n\x0cServerStatus\x12\x0f\n\x07running\x18\x01 \x01(\x08\x12\x0c\n\x04port\x18\x02 \x01(\rB\x13\xea\x02\x10Rakit::Generatedb\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Rakit
|
|
14
|
+
module Generated
|
|
15
|
+
StaticWebServerConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.static_web_server.StaticWebServerConfig").msgclass
|
|
16
|
+
PublishRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.static_web_server.PublishRequest").msgclass
|
|
17
|
+
PublishResult = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.static_web_server.PublishResult").msgclass
|
|
18
|
+
ServerStatus = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("rakit.static_web_server.ServerStatus").msgclass
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/rakit/azure/dev_ops.rb
CHANGED
data/lib/rakit/docfx.rb
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Rakit
|
|
6
|
+
# Build Docfx static documentation sites from source. Used by task :docfx and tests.
|
|
7
|
+
# Contract: specs/010-docfx/contracts/ruby-api.md
|
|
8
|
+
module Docfx
|
|
9
|
+
class << self
|
|
10
|
+
attr_accessor :docfx_path
|
|
11
|
+
|
|
12
|
+
def docfx_path
|
|
13
|
+
@docfx_path ||= "docfx"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param source_dir [String] path to Docfx source (must exist and be a directory)
|
|
17
|
+
# @param output_dir [String] path for build output
|
|
18
|
+
# @return [true] on success
|
|
19
|
+
# @return [false] on failure (Docfx not found, build failed, or invalid paths). Idempotent: repeated runs overwrite output_dir.
|
|
20
|
+
def build(source_dir:, output_dir:)
|
|
21
|
+
source_dir = ::File.expand_path(source_dir)
|
|
22
|
+
output_dir = ::File.expand_path(output_dir)
|
|
23
|
+
return false unless ::File.directory?(source_dir)
|
|
24
|
+
return false if ::File.file?(source_dir)
|
|
25
|
+
|
|
26
|
+
# Ensure output_dir can be created (parent writable)
|
|
27
|
+
parent = ::File.dirname(output_dir)
|
|
28
|
+
return false unless parent != output_dir
|
|
29
|
+
begin
|
|
30
|
+
FileUtils.mkdir_p(output_dir)
|
|
31
|
+
rescue Errno::EACCES, Errno::EROFS, Errno::EEXIST, Errno::ENOTDIR
|
|
32
|
+
return false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
bin = docfx_path
|
|
36
|
+
resolved = ::File.absolute_path?(bin) && ::File.executable?(bin) ? bin : which(bin)
|
|
37
|
+
return false unless resolved
|
|
38
|
+
bin = resolved
|
|
39
|
+
|
|
40
|
+
out_abs = output_dir
|
|
41
|
+
success = Dir.chdir(source_dir) do
|
|
42
|
+
system(bin, "build", "-o", out_abs, out: $stdout, err: $stderr)
|
|
43
|
+
end
|
|
44
|
+
return false unless success
|
|
45
|
+
return false unless ::File.directory?(out_abs) && (Dir.entries(out_abs) - %w[. ..]).any?
|
|
46
|
+
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param source_dir [String] path to check
|
|
51
|
+
# @return [Boolean] true if docfx.json exists under source_dir
|
|
52
|
+
def valid_source?(source_dir)
|
|
53
|
+
dir = ::File.expand_path(source_dir)
|
|
54
|
+
return false unless ::File.directory?(dir)
|
|
55
|
+
::File.file?(::File.join(dir, "docfx.json"))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def which(cmd)
|
|
61
|
+
exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(::File::PATH_SEPARATOR) : [""]
|
|
62
|
+
ENV["PATH"].split(::File::PATH_SEPARATOR).each do |path|
|
|
63
|
+
exts.each do |ext|
|
|
64
|
+
exe = ::File.join(path, "#{cmd}#{ext}")
|
|
65
|
+
return exe if ::File.executable?(exe) && !::File.directory?(exe)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/rakit/gem.rb
CHANGED
|
@@ -67,6 +67,10 @@ module Rakit
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def self.version_published?(name, version)
|
|
70
|
+
# If a version >= this one is already published (e.g. latest is higher), treat as published.
|
|
71
|
+
latest = latest_remote_version(name)
|
|
72
|
+
return true if latest && ::Gem::Version.new(latest) >= ::Gem::Version.new(version)
|
|
73
|
+
|
|
70
74
|
begin
|
|
71
75
|
return true if version_published_gem_list?(name, version)
|
|
72
76
|
rescue StandardError
|
|
@@ -85,6 +89,21 @@ module Rakit
|
|
|
85
89
|
false
|
|
86
90
|
end
|
|
87
91
|
|
|
92
|
+
# Return the latest version string for name on the remote, or nil on error.
|
|
93
|
+
def self.latest_remote_version(name)
|
|
94
|
+
out, _err, status = Open3.capture3("gem", "list", name, "--remote")
|
|
95
|
+
return nil unless status.success?
|
|
96
|
+
# Format: "name (1.0.0)" for latest only, or "name (1.0.0, 0.9.0)" with --all
|
|
97
|
+
out.each_line do |line|
|
|
98
|
+
next unless line.include?(name)
|
|
99
|
+
if line =~ /\s*#{Regexp.escape(name)}\s*\(([^)]+)\)/
|
|
100
|
+
versions = Regexp.last_match(1).split(",").map(&:strip)
|
|
101
|
+
return versions.max { |a, b| ::Gem::Version.new(a) <=> ::Gem::Version.new(b) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
88
107
|
# Run `gem list NAME --remote` and check if version appears in the output.
|
|
89
108
|
def self.version_published_gem_list?(name, version)
|
|
90
109
|
out, err, status = Open3.capture3("gem", "list", name, "--remote")
|
data/lib/rakit/hugo.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Rakit
|
|
6
|
+
# Build Hugo static sites from source. Used by publish_docs and tests.
|
|
7
|
+
# Contract: specs/008-hugo-docs-site/contracts/ruby-api.md
|
|
8
|
+
module Hugo
|
|
9
|
+
class << self
|
|
10
|
+
attr_accessor :hugo_path
|
|
11
|
+
|
|
12
|
+
def hugo_path
|
|
13
|
+
@hugo_path ||= "hugo"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param site_dir [String] path to Hugo source (must exist and be a directory)
|
|
17
|
+
# @param out_dir [String] path for build output
|
|
18
|
+
# @return [true] on success
|
|
19
|
+
# @return [false] on failure (Hugo not found, build failed, or invalid site_dir)
|
|
20
|
+
def build(site_dir:, out_dir:)
|
|
21
|
+
site_dir = ::File.expand_path(site_dir)
|
|
22
|
+
out_dir = ::File.expand_path(out_dir)
|
|
23
|
+
return false unless ::File.directory?(site_dir)
|
|
24
|
+
return false if ::File.file?(site_dir)
|
|
25
|
+
|
|
26
|
+
FileUtils.mkdir_p(out_dir)
|
|
27
|
+
success = system(hugo_path, "-s", site_dir, "-d", out_dir, out: $stdout, err: $stderr)
|
|
28
|
+
return false unless success
|
|
29
|
+
return false unless ::File.directory?(out_dir) && (Dir.entries(out_dir) - %w[. ..]).any?
|
|
30
|
+
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param site_dir [String] path to check
|
|
35
|
+
# @return [Boolean] true if config.toml or config.yaml exists under site_dir
|
|
36
|
+
def valid_site?(site_dir)
|
|
37
|
+
dir = ::File.expand_path(site_dir)
|
|
38
|
+
return false unless ::File.directory?(dir)
|
|
39
|
+
::File.file?(::File.join(dir, "config.toml")) || ::File.file?(::File.join(dir, "config.yaml"))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/rakit/markdown.rb
CHANGED
|
@@ -1,90 +1,241 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "pathname"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "generated/rakit.markdown_pb"
|
|
6
|
+
require "kramdown"
|
|
5
7
|
|
|
6
8
|
module Rakit
|
|
9
|
+
# Modular document amalgamate, disaggregate, validate (009); merge (legacy); generate_pdf.
|
|
10
|
+
# Document and Section are defined in proto/rakit.markdown.proto (generated in lib/generated/rakit.markdown_pb.rb).
|
|
11
|
+
# See specs/009-markdown-modular-docs/contracts/ruby-api.md
|
|
7
12
|
module Markdown
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
# Derive slug from section title: lowercase, non-alphanumerics → '-', collapse '-', trim.
|
|
14
|
+
def self.slug_from_title(title)
|
|
15
|
+
return "" if title.nil? || title.to_s.strip.empty?
|
|
16
|
+
s = title.to_s.downcase
|
|
17
|
+
s = s.gsub(/[^a-z0-9]+/, "-")
|
|
18
|
+
s = s.gsub(/-+/, "-")
|
|
19
|
+
s = s.sub(/\A-+/, "").sub(/-+\z/, "")
|
|
20
|
+
s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Discover .md section files under root_dir, sort by path. Empty → raise.
|
|
24
|
+
def self.discover_section_paths(root_dir)
|
|
25
|
+
root = ::File.expand_path(root_dir)
|
|
26
|
+
raise ArgumentError, "root_dir does not exist: #{root_dir}" unless ::Dir.exist?(root)
|
|
27
|
+
paths = ::Dir.glob(::File.join(root, "**", "*.md")).sort
|
|
28
|
+
paths.reject! { |p| ::File.basename(p).start_with?(".") }
|
|
29
|
+
raise ArgumentError, "no section files under root_dir (at least one .md required): #{root_dir}" if paths.empty?
|
|
30
|
+
paths
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.load_document(root_dir:)
|
|
34
|
+
path = root_dir.to_s
|
|
35
|
+
raise ArgumentError, "root_dir does not exist: #{path}" unless ::File.exist?(path)
|
|
36
|
+
raise ArgumentError, "root_dir is not a directory: #{path}" unless ::File.directory?(path)
|
|
37
|
+
full_paths = discover_section_paths(path)
|
|
38
|
+
root = ::File.expand_path(path)
|
|
39
|
+
title = ::File.basename(root)
|
|
40
|
+
sections = full_paths.map do |fp|
|
|
41
|
+
rel = Pathname.new(fp).relative_path_from(Pathname.new(root)).to_s
|
|
42
|
+
body = ::File.read(fp, encoding: "UTF-8")
|
|
43
|
+
base = ::File.basename(fp, ".md")
|
|
44
|
+
id = slug_from_title(base.sub(/\A\d+-/, "").tr("_", "-"))
|
|
45
|
+
id = "section" if id.empty?
|
|
46
|
+
Section.new(id: id, title: base, body: body, source_path: rel, sections: [])
|
|
47
|
+
end
|
|
48
|
+
Document.new(title: title, root_dir: path, sections: sections)
|
|
49
|
+
end
|
|
25
50
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
51
|
+
def self.amalgamate(document:, output_path:)
|
|
52
|
+
raise ArgumentError, "document is required" if document.nil?
|
|
53
|
+
out = output_path.to_s
|
|
54
|
+
buf = +""
|
|
55
|
+
buf << "# #{document.title}\n\n"
|
|
56
|
+
document.sections.each do |sec|
|
|
57
|
+
buf << "## #{sec.title}\n\n"
|
|
58
|
+
buf << sec.body.strip
|
|
59
|
+
buf << "\n\n" unless sec.body.strip.empty?
|
|
60
|
+
end
|
|
61
|
+
::File.write(out, buf, encoding: "UTF-8")
|
|
62
|
+
true
|
|
63
|
+
rescue => e
|
|
64
|
+
raise e
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.disaggregate(markdown_path:, output_root_dir:)
|
|
68
|
+
path = markdown_path.to_s
|
|
69
|
+
raise ArgumentError, "markdown_path does not exist: #{path}" unless ::File.exist?(path)
|
|
70
|
+
raise ArgumentError, "markdown_path is not a file: #{path}" unless ::File.file?(path)
|
|
71
|
+
content = ::File.read(path, encoding: "UTF-8")
|
|
72
|
+
sections = parse_headings_to_sections(content)
|
|
73
|
+
out_root = ::File.expand_path(output_root_dir.to_s)
|
|
74
|
+
parent = ::File.dirname(out_root)
|
|
75
|
+
tmp_dir = parent + "/.rakit_disaggregate_#{Process.pid}_#{rand(1_000_000)}"
|
|
76
|
+
::FileUtils.mkdir_p(tmp_dir)
|
|
77
|
+
begin
|
|
78
|
+
sections.each_with_index do |sec, i|
|
|
79
|
+
slug = sec[:slug].to_s.empty? ? "section" : sec[:slug]
|
|
80
|
+
fn = "#{slug}.md"
|
|
81
|
+
fn = "#{i}-#{fn}" if sections.size > 1 && slug == "section"
|
|
82
|
+
out_path = ::File.join(tmp_dir, fn)
|
|
83
|
+
::File.write(out_path, sec[:body].to_s.strip + "\n", encoding: "UTF-8")
|
|
29
84
|
end
|
|
85
|
+
::FileUtils.rm_rf(out_root) if ::Dir.exist?(out_root)
|
|
86
|
+
::FileUtils.mv(tmp_dir, out_root)
|
|
87
|
+
rescue => e
|
|
88
|
+
::FileUtils.rm_rf(tmp_dir) if ::Dir.exist?(tmp_dir)
|
|
89
|
+
raise e
|
|
90
|
+
end
|
|
91
|
+
true
|
|
92
|
+
end
|
|
30
93
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
94
|
+
# Parse markdown by headings ^#+\s+; return array of { level, title, slug, body }.
|
|
95
|
+
def self.parse_headings_to_sections(content)
|
|
96
|
+
lines = content.to_s.lines
|
|
97
|
+
heading_re = /\A(#+)\s+([^\n]*)\s*\z/
|
|
98
|
+
sections = []
|
|
99
|
+
current = { level: 0, title: nil, slug: nil, body: +"" }
|
|
100
|
+
found_any = false
|
|
101
|
+
lines.each do |line|
|
|
102
|
+
m = line.match(heading_re)
|
|
103
|
+
if m
|
|
104
|
+
found_any = true
|
|
105
|
+
level = m[1].length
|
|
106
|
+
title = m[2].strip
|
|
107
|
+
if current[:title]
|
|
108
|
+
current[:body] = current[:body].sub(/\n+\z/, "")
|
|
109
|
+
sections << { level: current[:level], title: current[:title], slug: slug_from_title(current[:title]), body: current[:body] }
|
|
41
110
|
end
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{ success: false, message: e.message, exit_code: 1, stderr: "" }
|
|
46
|
-
ensure
|
|
47
|
-
::File.unlink(temp_html) if temp_html && ::File.file?(temp_html)
|
|
111
|
+
current = { level: level, title: title, slug: slug_from_title(title), body: +"" }
|
|
112
|
+
else
|
|
113
|
+
current[:body] << line
|
|
48
114
|
end
|
|
49
115
|
end
|
|
116
|
+
current[:body] = current[:body].sub(/\n+\z/, "")
|
|
117
|
+
if current[:title] || !found_any
|
|
118
|
+
sections << { level: current[:level], title: current[:title] || "Section", slug: (current[:title] ? slug_from_title(current[:title]) : "section"), body: current[:body] }
|
|
119
|
+
end
|
|
120
|
+
sections = [{ level: 0, title: "Section", slug: "section", body: content.to_s.strip }] if sections.empty?
|
|
121
|
+
sections
|
|
122
|
+
end
|
|
50
123
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
124
|
+
class << self
|
|
125
|
+
attr_accessor :validation_failures
|
|
126
|
+
end
|
|
127
|
+
self.validation_failures = []
|
|
128
|
+
|
|
129
|
+
def self.validate(root_dir:)
|
|
130
|
+
self.validation_failures = []
|
|
131
|
+
doc = load_document(root_dir: root_dir)
|
|
132
|
+
ids = []
|
|
133
|
+
doc.sections.each do |sec|
|
|
134
|
+
ids << sec.id
|
|
135
|
+
end
|
|
136
|
+
seen = {}
|
|
137
|
+
ids.each do |id|
|
|
138
|
+
if seen[id]
|
|
139
|
+
self.validation_failures << "Duplicate section id: #{id}"
|
|
140
|
+
else
|
|
141
|
+
seen[id] = true
|
|
66
142
|
end
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
143
|
+
end
|
|
144
|
+
return false if validation_failures.any?
|
|
145
|
+
true
|
|
146
|
+
rescue ArgumentError => e
|
|
147
|
+
self.validation_failures = [e.message]
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ---- Legacy API: merge (concatenate .md files) and generate_pdf ----
|
|
152
|
+
|
|
153
|
+
# Merge: collect all .md under source_dir (sorted by path), write concatenated content to target_path.
|
|
154
|
+
# Target must be outside source_dir. Returns { success:, message:, exit_code: }.
|
|
155
|
+
# @param source_dir [String] directory to collect .md files from
|
|
156
|
+
# @param target_path [String] path to output markdown file
|
|
157
|
+
# @param create_directories [Boolean] if true, create parent dirs of target_path
|
|
158
|
+
def self.merge(source_dir, target_path, create_directories: false)
|
|
159
|
+
validated_dir = validate_dir_path(source_dir)
|
|
160
|
+
unless validated_dir
|
|
161
|
+
return { success: false, message: "Source directory must exist and be a directory", exit_code: 1 }
|
|
162
|
+
end
|
|
163
|
+
target_norm = normalize_path(target_path)
|
|
164
|
+
unless target_norm
|
|
165
|
+
return { success: false, message: "Target path is required", exit_code: 1 }
|
|
166
|
+
end
|
|
167
|
+
unless target_outside_source?(validated_dir, target_norm)
|
|
168
|
+
return { success: false, message: "Target path must be outside source directory", exit_code: 1 }
|
|
169
|
+
end
|
|
170
|
+
parent = ::File.dirname(target_norm)
|
|
171
|
+
unless ::File.directory?(parent)
|
|
172
|
+
if create_directories
|
|
173
|
+
::FileUtils.mkdir_p(parent)
|
|
174
|
+
else
|
|
175
|
+
return { success: false, message: "Parent directory does not exist; use create_directories: true", exit_code: 1 }
|
|
74
176
|
end
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
177
|
+
end
|
|
178
|
+
begin
|
|
179
|
+
content = merge_collect(validated_dir)
|
|
180
|
+
::File.write(target_norm, content, encoding: "UTF-8")
|
|
181
|
+
{ success: true, message: "", exit_code: 0 }
|
|
182
|
+
rescue => e
|
|
183
|
+
::File.unlink(target_norm) if ::File.file?(target_norm)
|
|
184
|
+
{ success: false, message: e.message, exit_code: 1 }
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Generate PDF from a markdown file (requires weasyprint or wkhtmltopdf on PATH; on Mac/Linux weasyprint is preferred).
|
|
189
|
+
# @param source_path [String] path to markdown file
|
|
190
|
+
# @param output_path [String, nil] path for PDF; default same dir, .pdf extension
|
|
191
|
+
# @return [Hash] { success:, message:, exit_code:, stderr: }
|
|
192
|
+
def self.generate_pdf(source_path, output_path = nil)
|
|
193
|
+
validated = validate_file_path(source_path)
|
|
194
|
+
unless validated
|
|
195
|
+
return { success: false, message: "Source path must exist and be a file", exit_code: 1, stderr: "" }
|
|
196
|
+
end
|
|
197
|
+
out_path = if output_path.to_s.strip.empty?
|
|
198
|
+
nil
|
|
199
|
+
else
|
|
200
|
+
::File.expand_path(output_path.to_s.strip)
|
|
201
|
+
end
|
|
202
|
+
out_path ||= ::File.join(::File.dirname(validated), ::File.basename(validated, ".*") + ".pdf")
|
|
203
|
+
out_path = ::File.expand_path(out_path)
|
|
204
|
+
|
|
205
|
+
engine = pdf_engine_available?
|
|
206
|
+
unless engine
|
|
207
|
+
msg = pdf_engine_install_message
|
|
208
|
+
return { success: false, message: msg, exit_code: 1, stderr: msg }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
temp_html = nil
|
|
212
|
+
begin
|
|
213
|
+
md_content = ::File.read(validated, encoding: "UTF-8")
|
|
214
|
+
html = wrap_html_for_pdf(markdown_to_html(md_content))
|
|
215
|
+
temp_html = ::File.join(::File.dirname(out_path), ".rakit_md_#{Process.pid}_#{object_id}.html")
|
|
216
|
+
::File.write(temp_html, html, encoding: "UTF-8")
|
|
217
|
+
ok = run_pdf_engine(engine, temp_html, out_path)
|
|
218
|
+
unless ok
|
|
219
|
+
::File.unlink(out_path) if ::File.file?(out_path)
|
|
220
|
+
return { success: false, message: "#{engine} failed", exit_code: 1, stderr: "" }
|
|
82
221
|
end
|
|
222
|
+
{ success: true, message: "", exit_code: 0, stderr: "" }
|
|
223
|
+
rescue => e
|
|
224
|
+
::File.unlink(out_path) if out_path && ::File.file?(out_path)
|
|
225
|
+
{ success: false, message: e.message, exit_code: 1, stderr: "" }
|
|
226
|
+
ensure
|
|
227
|
+
::File.unlink(temp_html) if temp_html && ::File.file?(temp_html)
|
|
83
228
|
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# True if a PDF engine (weasyprint or wkhtmltopdf) is available for generate_pdf.
|
|
232
|
+
def self.pdf_available?
|
|
233
|
+
send(:pdf_engine_available?).nil? ? false : true
|
|
234
|
+
end
|
|
84
235
|
|
|
236
|
+
class << self
|
|
85
237
|
private
|
|
86
238
|
|
|
87
|
-
# T004: Normalize path; return nil if nil/empty after strip.
|
|
88
239
|
def normalize_path(path, base_dir = nil)
|
|
89
240
|
return nil if path.nil?
|
|
90
241
|
s = path.to_s.strip
|
|
@@ -92,7 +243,6 @@ module Rakit
|
|
|
92
243
|
base_dir ? ::File.expand_path(s, base_dir) : ::File.expand_path(s)
|
|
93
244
|
end
|
|
94
245
|
|
|
95
|
-
# T004: Validate path exists and is a file. Return expanded path or nil.
|
|
96
246
|
def validate_file_path(path)
|
|
97
247
|
norm = normalize_path(path)
|
|
98
248
|
return nil unless norm
|
|
@@ -100,7 +250,6 @@ module Rakit
|
|
|
100
250
|
norm
|
|
101
251
|
end
|
|
102
252
|
|
|
103
|
-
# T004: Validate path exists and is a directory. Return expanded path or nil.
|
|
104
253
|
def validate_dir_path(path)
|
|
105
254
|
norm = normalize_path(path)
|
|
106
255
|
return nil unless norm
|
|
@@ -108,7 +257,6 @@ module Rakit
|
|
|
108
257
|
norm
|
|
109
258
|
end
|
|
110
259
|
|
|
111
|
-
# T005: Target is outside source_dir (not equal, not under). Both should be normalized.
|
|
112
260
|
def target_outside_source?(source_dir, target_path)
|
|
113
261
|
src = source_dir.to_s.gsub(::File::SEPARATOR, "/").chomp("/")
|
|
114
262
|
target = ::File.expand_path(target_path).to_s.gsub(::File::SEPARATOR, "/")
|
|
@@ -119,38 +267,82 @@ module Rakit
|
|
|
119
267
|
true
|
|
120
268
|
end
|
|
121
269
|
|
|
122
|
-
|
|
270
|
+
def mac_or_linux?
|
|
271
|
+
RUBY_PLATFORM.include?("darwin") || RUBY_PLATFORM.include?("linux")
|
|
272
|
+
end
|
|
273
|
+
|
|
123
274
|
def wkhtmltopdf_available?
|
|
124
|
-
|
|
275
|
+
`wkhtmltopdf --version 2>&1`
|
|
276
|
+
$?.success?
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def weasyprint_available?
|
|
280
|
+
`weasyprint --version 2>&1`
|
|
125
281
|
$?.success?
|
|
126
282
|
end
|
|
127
283
|
|
|
128
|
-
#
|
|
129
|
-
def
|
|
284
|
+
# Returns :weasyprint or :wkhtmltopdf if available. On Mac/Linux prefers weasyprint.
|
|
285
|
+
def pdf_engine_available?
|
|
286
|
+
if mac_or_linux?
|
|
287
|
+
return :weasyprint if weasyprint_available?
|
|
288
|
+
return :wkhtmltopdf if wkhtmltopdf_available?
|
|
289
|
+
else
|
|
290
|
+
return :wkhtmltopdf if wkhtmltopdf_available?
|
|
291
|
+
return :weasyprint if weasyprint_available?
|
|
292
|
+
end
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def pdf_engine_install_message
|
|
130
297
|
if ::Gem.win_platform?
|
|
131
298
|
"Install wkhtmltopdf: choco install wkhtmltopdf"
|
|
132
|
-
elsif
|
|
133
|
-
"Install
|
|
299
|
+
elsif mac_or_linux?
|
|
300
|
+
"Install weasyprint: brew install weasyprint (macOS) or apt install weasyprint (Debian/Ubuntu). Alternatively: wkhtmltopdf."
|
|
301
|
+
else
|
|
302
|
+
"Install wkhtmltopdf or weasyprint for PDF generation."
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def run_pdf_engine(engine, html_path, pdf_path)
|
|
307
|
+
case engine
|
|
308
|
+
when :weasyprint
|
|
309
|
+
system("weasyprint", "-e", "UTF-8", html_path, pdf_path, out: ::File::NULL, err: ::File::NULL)
|
|
310
|
+
when :wkhtmltopdf
|
|
311
|
+
system("wkhtmltopdf", "-q", "--encoding", "UTF-8", html_path, pdf_path, out: ::File::NULL, err: ::File::NULL)
|
|
134
312
|
else
|
|
135
|
-
|
|
313
|
+
false
|
|
136
314
|
end
|
|
137
315
|
end
|
|
138
316
|
|
|
139
|
-
# T007: Convert markdown string to HTML. Empty input -> minimal HTML.
|
|
140
317
|
def markdown_to_html(md_string)
|
|
141
318
|
return "" if md_string.nil? || md_string.to_s.strip.empty?
|
|
142
319
|
Kramdown::Document.new(md_string.to_s).to_html
|
|
143
320
|
end
|
|
144
321
|
|
|
145
|
-
|
|
146
|
-
|
|
322
|
+
def wrap_html_for_pdf(html_fragment)
|
|
323
|
+
<<~HTML
|
|
324
|
+
<!DOCTYPE html>
|
|
325
|
+
<html>
|
|
326
|
+
<head>
|
|
327
|
+
<meta charset="UTF-8">
|
|
328
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
329
|
+
<style>body { font-size: 200%; }</style>
|
|
330
|
+
</head>
|
|
331
|
+
<body>
|
|
332
|
+
#{html_fragment}
|
|
333
|
+
</body>
|
|
334
|
+
</html>
|
|
335
|
+
HTML
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
MERGE_SEPARATOR = "\n\n"
|
|
147
339
|
|
|
148
|
-
def
|
|
340
|
+
def merge_collect(source_dir)
|
|
149
341
|
norm_dir = ::File.expand_path(source_dir)
|
|
150
342
|
pattern = ::File.join(norm_dir, "**", "*.md")
|
|
151
343
|
paths = ::Dir.glob(pattern).select { |p| ::File.file?(p) }.sort
|
|
152
344
|
return "" if paths.empty?
|
|
153
|
-
paths.map { |p| ::File.read(p, encoding: "UTF-8") }.join(
|
|
345
|
+
paths.map { |p| ::File.read(p, encoding: "UTF-8") }.join(MERGE_SEPARATOR)
|
|
154
346
|
end
|
|
155
347
|
end
|
|
156
348
|
end
|
data/lib/rakit/shell.rb
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
-
require "generated/static_web_server_pb"
|
|
4
|
+
require "generated/rakit.static_web_server_pb"
|
|
5
5
|
|
|
6
6
|
module Rakit
|
|
7
|
-
# Publish static sites into a configurable root, regenerate a root index, and control a local HTTP server (start/stop/running).
|
|
8
|
-
#
|
|
7
|
+
# Publish static sites into a configurable root (~/.rakit/wwwroot), regenerate a root index, and control a local HTTP server (start/stop/running).
|
|
8
|
+
# Host and port are configurable (host default 127.0.0.1, port 5099) and returned regardless of server state for URL building.
|
|
9
|
+
# CLI: +rakit static-web-server+ (start|stop|running|publish|view). See specs/003-static-web-server/contracts/ruby-api.md and specs/007-wwwroot-docs-hugo-view/contracts/ruby-api.md.
|
|
9
10
|
module StaticWebServer
|
|
10
11
|
SITE_NAME_REGEX = /\A[a-z0-9\-]+\z/.freeze
|
|
11
12
|
|
|
12
13
|
class << self
|
|
13
|
-
# @return [String] Static root directory (default ~/.rakit/
|
|
14
|
+
# @return [String] Static root directory (default ~/.rakit/wwwroot). Used by publish and server lifecycle.
|
|
14
15
|
attr_accessor :root
|
|
15
|
-
# @return [
|
|
16
|
+
# @return [String] Host for building URLs (default 127.0.0.1). Returned regardless of server running state.
|
|
17
|
+
attr_accessor :host
|
|
18
|
+
# @return [Integer] Default port for start (default 5099). Used when no override passed to start; returned regardless of server running state.
|
|
16
19
|
attr_accessor :port
|
|
17
20
|
end
|
|
18
21
|
|
|
19
|
-
self.root = ::File.expand_path("~/.rakit/
|
|
22
|
+
self.root = ::File.expand_path("~/.rakit/wwwroot")
|
|
23
|
+
self.host = "127.0.0.1"
|
|
20
24
|
self.port = 5099
|
|
21
25
|
|
|
22
26
|
# T005: Validate site name; raise ArgumentError before any filesystem write.
|
|
27
|
+
# Single segment (e.g. "mysite") or multi-segment path (e.g. "louparslow/rakit"); each segment must match SITE_NAME_REGEX.
|
|
23
28
|
def self.validate_site_name!(site_name)
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
validate_site_path!(site_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.validate_site_path!(site_name)
|
|
33
|
+
return if site_name.nil? || site_name.to_s.empty?
|
|
34
|
+
site_name.to_s.split(::File::SEPARATOR).each do |seg|
|
|
35
|
+
next if seg.empty?
|
|
36
|
+
unless seg.match?(SITE_NAME_REGEX)
|
|
37
|
+
raise ArgumentError, "site_name segment must be lowercase alphanumeric and dashes only (e.g. my-site); got: #{seg.inspect}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
26
40
|
end
|
|
27
41
|
|
|
28
42
|
# T006: PID file path for start/stop/running (research: ~/.rakit/static_web_server.pid).
|
|
@@ -49,26 +63,27 @@ module Rakit
|
|
|
49
63
|
end
|
|
50
64
|
|
|
51
65
|
# Publish static content from source_directory to root/site_name (atomic copy), then regenerate root index.
|
|
52
|
-
# @param site_name [String]
|
|
66
|
+
# @param site_name [String] Single segment (e.g. "mysite") or path (e.g. "louparslow/rakit"); each segment must match \\A[a-z0-9\-]+\\z.
|
|
53
67
|
# @param source_directory [String] Existing directory path; contents are copied (no traversal outside allowed paths).
|
|
54
68
|
# @return [true] on success.
|
|
55
69
|
# @raise [ArgumentError] for invalid site_name, missing/invalid source_directory, or root not writable.
|
|
56
70
|
def self.publish(site_name, source_directory)
|
|
57
|
-
|
|
71
|
+
validate_site_path!(site_name)
|
|
58
72
|
src = ::File.expand_path(source_directory)
|
|
59
73
|
raise ArgumentError, "source_directory does not exist: #{source_directory}" unless ::File.exist?(src)
|
|
60
74
|
raise ArgumentError, "source_directory is not a directory: #{source_directory}" unless ::File.directory?(src)
|
|
61
75
|
|
|
62
76
|
ensure_root
|
|
63
|
-
# Check root is writable before we do any copy (T015 / edge case).
|
|
64
77
|
unless ::File.writable?(root)
|
|
65
78
|
raise ArgumentError, "root directory is not writable: #{root}"
|
|
66
79
|
end
|
|
67
80
|
|
|
68
81
|
target = ::File.join(root, site_name)
|
|
69
|
-
|
|
82
|
+
temp_name = ".tmp_#{site_name.to_s.gsub(::File::SEPARATOR, '_')}_#{Process.pid}_#{rand(1_000_000)}"
|
|
83
|
+
temp = ::File.join(root, temp_name)
|
|
70
84
|
FileUtils.mkdir_p(temp)
|
|
71
85
|
FileUtils.cp_r(::File.join(src, "."), temp)
|
|
86
|
+
FileUtils.mkdir_p(::File.dirname(target))
|
|
72
87
|
FileUtils.rm_rf(target)
|
|
73
88
|
FileUtils.mv(temp, target)
|
|
74
89
|
regenerate_root_index
|
|
@@ -95,6 +110,7 @@ module Rakit
|
|
|
95
110
|
def self.start(options = {})
|
|
96
111
|
return true if running?
|
|
97
112
|
ensure_root
|
|
113
|
+
regenerate_root_index # so root / (Sites page) lists current nested sites
|
|
98
114
|
p = options[:port] || port
|
|
99
115
|
raise "port #{p} is already in use; change port or stop the other process" if port_in_use?(p)
|
|
100
116
|
bin = server_binary
|
|
@@ -109,19 +125,23 @@ module Rakit
|
|
|
109
125
|
|
|
110
126
|
STOP_TIMEOUT = 5
|
|
111
127
|
|
|
112
|
-
# Gracefully terminate server process; remove PID file.
|
|
113
|
-
# @return [true] if stopped.
|
|
114
|
-
# @raise [RuntimeError] if server was not running (no PID file or process not found).
|
|
128
|
+
# Gracefully terminate server process; remove PID file. Idempotent: if not running (no PID file or process not found), no-op and return true.
|
|
129
|
+
# @return [true] if stopped or already not running.
|
|
115
130
|
def self.stop
|
|
116
131
|
path = pid_file_path
|
|
117
|
-
|
|
132
|
+
unless ::File.file?(path)
|
|
133
|
+
return true # already not running
|
|
134
|
+
end
|
|
118
135
|
pid = ::File.read(path).strip.to_i
|
|
119
|
-
|
|
136
|
+
if pid <= 0
|
|
137
|
+
::File.delete(path) rescue nil
|
|
138
|
+
return true
|
|
139
|
+
end
|
|
120
140
|
begin
|
|
121
141
|
Process.getpgid(pid)
|
|
122
142
|
rescue Errno::ESRCH
|
|
123
|
-
::File.delete(path)
|
|
124
|
-
|
|
143
|
+
::File.delete(path) rescue nil
|
|
144
|
+
return true # process already gone; treat as stopped
|
|
125
145
|
end
|
|
126
146
|
Process.kill(:TERM, pid)
|
|
127
147
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + STOP_TIMEOUT
|
|
@@ -145,17 +165,49 @@ module Rakit
|
|
|
145
165
|
true
|
|
146
166
|
end
|
|
147
167
|
|
|
148
|
-
#
|
|
168
|
+
# Open a path on the static server in the default browser. Ensures server is running (starts if not), builds URL, launches browser.
|
|
169
|
+
# @param relative_path [String] URL path (e.g. "/louparslow/rakit/" or "louparslow/rakit"); normalized to one leading slash.
|
|
170
|
+
# @return [true] on success.
|
|
171
|
+
# @raise [RuntimeError] if browser cannot be launched (e.g. headless), with message e.g. "Could not launch browser; display required?"
|
|
172
|
+
def self.view(relative_path)
|
|
173
|
+
start unless running?
|
|
174
|
+
path = relative_path.to_s.strip
|
|
175
|
+
path = "/#{path}" unless path.empty? || path.start_with?("/")
|
|
176
|
+
path = "/" if path.empty?
|
|
177
|
+
url = "http://#{host}:#{port}#{path}"
|
|
178
|
+
$stdout.puts "Opening #{url}"
|
|
179
|
+
launch_browser(url)
|
|
180
|
+
true
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# T017: Write root/index.html listing all subdirectories that have index.html (or index.xml) with links.
|
|
149
184
|
def self.regenerate_root_index
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
185
|
+
paths = []
|
|
186
|
+
_collect_index_paths(root, "", paths)
|
|
187
|
+
paths.sort!
|
|
153
188
|
html = ["<!DOCTYPE html>", "<html><head><meta charset=\"utf-8\"><title>Published sites</title></head><body>", "<h1>Published sites</h1>", "<ul>"]
|
|
154
|
-
|
|
189
|
+
paths.each do |rel|
|
|
190
|
+
name = rel.chomp("/").split("/").last || rel
|
|
191
|
+
html << "<li><a href=\"/#{rel}\">#{name} (#{rel})</a></li>"
|
|
192
|
+
end
|
|
155
193
|
html << "</ul></body></html>"
|
|
156
194
|
::File.write(::File.join(root, "index.html"), html.join("\n"))
|
|
157
195
|
end
|
|
158
196
|
|
|
197
|
+
# Recursively find relative paths under dir (relative to root) that contain index.html or index.xml.
|
|
198
|
+
def self._collect_index_paths(dir, rel, out)
|
|
199
|
+
Dir.entries(dir).sort.each do |e|
|
|
200
|
+
next if e == "." || e == ".." || e.start_with?(".")
|
|
201
|
+
full = ::File.join(dir, e)
|
|
202
|
+
next unless ::File.directory?(full)
|
|
203
|
+
sub_rel = rel.empty? ? e : "#{rel}/#{e}"
|
|
204
|
+
index_html = ::File.join(full, "index.html")
|
|
205
|
+
index_xml = ::File.join(full, "index.xml")
|
|
206
|
+
out << "#{sub_rel}/" if ::File.file?(index_html) || ::File.file?(index_xml)
|
|
207
|
+
_collect_index_paths(full, sub_rel, out)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
159
211
|
class << self
|
|
160
212
|
private
|
|
161
213
|
|
|
@@ -178,6 +230,20 @@ module Rakit
|
|
|
178
230
|
rescue Errno::EADDRINUSE
|
|
179
231
|
true
|
|
180
232
|
end
|
|
233
|
+
|
|
234
|
+
def launch_browser(url)
|
|
235
|
+
cmd = case ::RbConfig::CONFIG["host_os"]
|
|
236
|
+
when /darwin|mac os/i
|
|
237
|
+
["open", url]
|
|
238
|
+
when /mswin|mingw|windows/i
|
|
239
|
+
["cmd", "/c", "start", "", url]
|
|
240
|
+
else
|
|
241
|
+
["xdg-open", url]
|
|
242
|
+
end
|
|
243
|
+
system(*cmd, out: ::File::NULL, err: ::File::NULL)
|
|
244
|
+
return true if $?.success?
|
|
245
|
+
raise "Could not launch browser; display required?"
|
|
246
|
+
end
|
|
181
247
|
end
|
|
182
248
|
end
|
|
183
249
|
end
|
data/lib/rakit.rb
CHANGED
|
@@ -25,6 +25,9 @@ require_relative "rakit/protobuf"
|
|
|
25
25
|
# Defer loading so rake tasks that don't need Shell (e.g. clobber) work without google-protobuf.
|
|
26
26
|
autoload :Shell, "rakit/shell"
|
|
27
27
|
require_relative "rakit/static_web_server"
|
|
28
|
+
require_relative "rakit/docfx"
|
|
29
|
+
require_relative "rakit/hugo"
|
|
30
|
+
require_relative "rakit/markdown"
|
|
28
31
|
require_relative "rakit/word_count"
|
|
29
32
|
require_relative "rakit/word_cloud"
|
|
30
33
|
require_relative "rakit/file"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rakit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rakit
|
|
@@ -122,7 +122,13 @@ files:
|
|
|
122
122
|
- lib/generated/azure.devops_pb.rb
|
|
123
123
|
- lib/generated/data_pb.rb
|
|
124
124
|
- lib/generated/example_pb.rb
|
|
125
|
+
- lib/generated/rakit.azure_pb.rb
|
|
126
|
+
- lib/generated/rakit.docfx_pb.rb
|
|
127
|
+
- lib/generated/rakit.example_pb.rb
|
|
125
128
|
- lib/generated/rakit.file_pb.rb
|
|
129
|
+
- lib/generated/rakit.markdown_pb.rb
|
|
130
|
+
- lib/generated/rakit.shell_pb.rb
|
|
131
|
+
- lib/generated/rakit.static_web_server_pb.rb
|
|
126
132
|
- lib/generated/rakit.word_count_pb.rb
|
|
127
133
|
- lib/generated/shell_pb.rb
|
|
128
134
|
- lib/generated/static_web_server_pb.rb
|
|
@@ -130,12 +136,13 @@ files:
|
|
|
130
136
|
- lib/rakit.rb
|
|
131
137
|
- lib/rakit/azure/dev_ops.rb
|
|
132
138
|
- lib/rakit/cli/file.rb
|
|
133
|
-
- lib/rakit/cli/markdown.rb
|
|
134
139
|
- lib/rakit/cli/word_cloud.rb
|
|
135
140
|
- lib/rakit/cli/word_count.rb
|
|
141
|
+
- lib/rakit/docfx.rb
|
|
136
142
|
- lib/rakit/file.rb
|
|
137
143
|
- lib/rakit/gem.rb
|
|
138
144
|
- lib/rakit/git.rb
|
|
145
|
+
- lib/rakit/hugo.rb
|
|
139
146
|
- lib/rakit/markdown.rb
|
|
140
147
|
- lib/rakit/protobuf.rb
|
|
141
148
|
- lib/rakit/shell.rb
|
data/lib/rakit/cli/markdown.rb
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rakit/markdown"
|
|
4
|
-
|
|
5
|
-
module Rakit
|
|
6
|
-
module CLI
|
|
7
|
-
module Markdown
|
|
8
|
-
class << self
|
|
9
|
-
# @param argv [Array<String>] arguments after "markdown"
|
|
10
|
-
# @return [Integer] exit code
|
|
11
|
-
def run(argv)
|
|
12
|
-
return 1 if argv.empty?
|
|
13
|
-
sub = argv.shift
|
|
14
|
-
case sub
|
|
15
|
-
when "pdf" then run_pdf(argv)
|
|
16
|
-
when "amalgamate" then run_amalgamate(argv)
|
|
17
|
-
else
|
|
18
|
-
$stderr.puts "Unknown command: #{sub}. Use 'pdf' or 'amalgamate'."
|
|
19
|
-
$stderr.puts " rakit markdown pdf <input.md> [output.pdf]"
|
|
20
|
-
$stderr.puts " rakit markdown amalgamate <source_dir> <target.md> [--create-directories]"
|
|
21
|
-
1
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
def run_pdf(argv)
|
|
28
|
-
opts = parse_pdf_argv(argv)
|
|
29
|
-
return 1 if opts[:error]
|
|
30
|
-
result = Rakit::Markdown.generate_pdf(opts[:input], opts[:output])
|
|
31
|
-
$stderr.puts result[:stderr] if result[:stderr] && !result[:stderr].empty? && !result[:success]
|
|
32
|
-
$stderr.puts result[:message] if result[:message] && !result[:message].empty? && !result[:success]
|
|
33
|
-
result[:exit_code] || (result[:success] ? 0 : 1)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def parse_pdf_argv(argv)
|
|
37
|
-
opts = { input: nil, output: nil, error: false }
|
|
38
|
-
args = argv.dup
|
|
39
|
-
pos = []
|
|
40
|
-
while (arg = args.shift)
|
|
41
|
-
if arg.start_with?("--")
|
|
42
|
-
$stderr.puts "Unknown option: #{arg}"
|
|
43
|
-
opts[:error] = true
|
|
44
|
-
else
|
|
45
|
-
pos << arg
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
opts[:input] = pos[0]
|
|
49
|
-
opts[:output] = pos[1] if pos.length > 1
|
|
50
|
-
unless opts[:input] && !opts[:input].empty?
|
|
51
|
-
$stderr.puts "pdf requires <input.md>"
|
|
52
|
-
opts[:error] = true
|
|
53
|
-
end
|
|
54
|
-
opts
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def run_amalgamate(argv)
|
|
58
|
-
opts = parse_amalgamate_argv(argv)
|
|
59
|
-
return 1 if opts[:error]
|
|
60
|
-
result = Rakit::Markdown.amalgamate(opts[:source_dir], opts[:target], create_directories: opts[:create_directories])
|
|
61
|
-
$stderr.puts result[:message] if result[:message] && !result[:message].empty? && !result[:success]
|
|
62
|
-
result[:exit_code] || (result[:success] ? 0 : 1)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def parse_amalgamate_argv(argv)
|
|
66
|
-
opts = { source_dir: nil, target: nil, create_directories: false, error: false }
|
|
67
|
-
args = argv.dup
|
|
68
|
-
pos = []
|
|
69
|
-
while (arg = args.shift)
|
|
70
|
-
case arg
|
|
71
|
-
when "--create-directories" then opts[:create_directories] = true
|
|
72
|
-
when /^--/
|
|
73
|
-
$stderr.puts "Unknown option: #{arg}"
|
|
74
|
-
opts[:error] = true
|
|
75
|
-
else
|
|
76
|
-
pos << arg
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
opts[:source_dir] = pos[0]
|
|
80
|
-
opts[:target] = pos[1]
|
|
81
|
-
unless opts[:source_dir] && opts[:target]
|
|
82
|
-
$stderr.puts "amalgamate requires <source_dir> and <target.md>"
|
|
83
|
-
opts[:error] = true
|
|
84
|
-
end
|
|
85
|
-
opts
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|