Skip to content

Commit

Permalink
Add project name and version to API docs (crystal-lang#8792)
Browse files Browse the repository at this point in the history
Introduces Crystal::Docs::ProjectInfo as container for doc generator configuration
  • Loading branch information
straight-shoota authored and carlhoerberg committed Apr 29, 2020
1 parent 4c2c44d commit f7dd352
Show file tree
Hide file tree
Showing 13 changed files with 384 additions and 21 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ LIB_CRYSTAL_TARGET = src/ext/libcrystal.a
DEPS = $(LLVM_EXT_OBJ) $(LIB_CRYSTAL_TARGET)
CFLAGS += -fPIC $(if $(debug),-g -O0)
CXXFLAGS += $(if $(debug),-g -O0)
CRYSTAL_VERSION ?= $(shell cat src/VERSION)

ifeq ($(shell command -v ld.lld >/dev/null && uname -s),Linux)
EXPORT_CC ?= CC="cc -fuse-ld=lld"
Expand Down Expand Up @@ -90,7 +91,7 @@ compiler_spec: $(O)/compiler_spec ## Run compiler specs

.PHONY: docs
docs: ## Generate standard library documentation
$(BUILD_PATH) ./bin/crystal docs src/docs_main.cr $(DOCS_OPTIONS)
$(BUILD_PATH) ./bin/crystal docs src/docs_main.cr $(DOCS_OPTIONS) --project-name=Crystal --project-version=$(CRYSTAL_VERSION)

.PHONY: crystal
crystal: $(O)/crystal ## Build the compiler
Expand Down
6 changes: 6 additions & 0 deletions man/crystal.1
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ Generate documentation from comments using a subset of markdown. The output is s
Options:
.Bl -tag -width "12345678" -compact
.Pp
.It Fl -project-name Ar NAME
Set the project name. The default value is extracted from shard.yml if available.
In case no default can be found, this option is mandatory.
.It Fl -project-version Ar VERSION
Set the project version. The default value is extracted from current git commit or shard.yml if available.
In case no default can be found, this option is mandatory.
.It Fl o Ar DIR, Fl -output Ar DIR
Set the output directory (default: ./docs).
.It Fl b Ar URL, Fl -sitemap-base-url Ar URL
Expand Down
204 changes: 204 additions & 0 deletions spec/compiler/crystal/tools/doc/project_info_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
require "../../../../spec_helper"
require "../../../../support/tempfile"

private alias ProjectInfo = Crystal::Doc::ProjectInfo

private def run_git(command)
Process.run(%(git -c user.email="" -c user.name="spec" #{command}), shell: true)
end

private def assert_with_defaults(initial, expected, *, file = __FILE__, line = __LINE__)
initial.fill_with_defaults
initial.should eq(expected), file: file, line: line
end

describe Crystal::Doc::ProjectInfo do
around_each do |example|
with_tempfile("docs-project") do |tempdir|
Dir.mkdir tempdir
Dir.cd(tempdir) do
example.run
end
end
end

describe ".new_with_defaults" do
it "empty folder" do
assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new(nil, nil))
assert_with_defaults(ProjectInfo.new("foo", "1.0"), ProjectInfo.new("foo", "1.0"))
end

context "with shard.yml" do
before_each do
File.write("shard.yml", "name: foo\nversion: 1.0")
end

it "no git" do
assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "1.0"))
assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0"))
assert_with_defaults(ProjectInfo.new(nil, "2.0"), ProjectInfo.new("foo", "2.0"))
end

it "git but no commit" do
run_git "init"

assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", nil))
assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0"))
assert_with_defaults(ProjectInfo.new(nil, "2.0"), ProjectInfo.new("foo", "2.0"))
end

it "git tagged version" do
run_git "init"
run_git "add shard.yml"
run_git "commit -m 'Initial commit' --no-gpg-sign"
run_git "tag v3.0"

assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "3.0"))
assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0"))
end

it "git tagged version dirty" do
run_git "init"
run_git "add shard.yml"
run_git "commit -m 'Initial commit' --no-gpg-sign"
run_git "tag v3.0"
File.write("foo.txt", "bar")

assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "3.0-dev"))
assert_with_defaults(ProjectInfo.new(nil, "1.1"), ProjectInfo.new("foo", "1.1"))
assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0"))
end

it "git non-tagged commit" do
run_git "init"
run_git "add shard.yml"
run_git "commit -m 'Initial commit' --no-gpg-sign"

assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "master"))
assert_with_defaults(ProjectInfo.new(nil, "1.1"), ProjectInfo.new("foo", "1.1"))
assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0"))
end

it "git non-tagged commit dirty" do
run_git "init"
run_git "add shard.yml"
run_git "commit -m 'Initial commit' --no-gpg-sign"
File.write("foo.txt", "bar")

assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "master-dev"))
assert_with_defaults(ProjectInfo.new(nil, "1.1"), ProjectInfo.new("foo", "1.1"))
assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0"))
end
end

it "no shard.yml, but git tagged version" do
File.write("foo.txt", "bar")
run_git "init"
run_git "add foo.txt"
run_git "commit -m 'Remove shard.yml' --no-gpg-sign"
run_git "tag v4.0"

assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new(nil, "4.0"))
assert_with_defaults(ProjectInfo.new("foo", nil), ProjectInfo.new("foo", "4.0"))
assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0"))
end
end

it ".find_git_version" do
# Non-git directory
ProjectInfo.find_git_version.should be_nil

# Empty git directory
run_git "init"
ProjectInfo.find_git_version.should be_nil

# Non-tagged commit
File.write("file.txt", "foo")
run_git "add file.txt"
run_git "commit -m 'Initial commit' --no-gpg-sign"
ProjectInfo.find_git_version.should eq "master"

# Non-tagged commit, dirty workdir
File.write("file.txt", "bar")
ProjectInfo.find_git_version.should eq "master-dev"

run_git "checkout -- ."

# Tagged commit
run_git "tag v0.1.0"
ProjectInfo.find_git_version.should eq "0.1.0"

# Tagged commit, dirty workdir
File.write("file.txt", "bar")
ProjectInfo.find_git_version.should eq "0.1.0-dev"

# Tagged commit, dirty index
run_git "add file.txt"
ProjectInfo.find_git_version.should eq "0.1.0-dev"

run_git "reset --hard v0.1.0"
ProjectInfo.find_git_version.should eq "0.1.0"

# Multiple tags
run_git "tag v0.2.0"
ProjectInfo.find_git_version.should eq "master"

# Other branch
run_git "checkout -b foo"
ProjectInfo.find_git_version.should eq "foo"
end

describe ".read_shard_properties" do
it "no shard.yml" do
ProjectInfo.read_shard_properties.should eq({nil, nil})
end

it "without name and version properties" do
File.write("shard.yml", "foo: bar\n")
ProjectInfo.read_shard_properties.should eq({nil, nil})
end

it "empty properties" do
File.write("shard.yml", "name: \nversion: ")
ProjectInfo.read_shard_properties.should eq({nil, nil})
end

it "indented properties" do
File.write("shard.yml", " name: bar\n version: 1.0")
ProjectInfo.read_shard_properties.should eq({nil, nil})
end

it "only name" do
File.write("shard.yml", "name: bar\n")
ProjectInfo.read_shard_properties.should eq({"bar", nil})
end

it "name and version" do
File.write("shard.yml", "name: bar\nversion: 1.0")
ProjectInfo.read_shard_properties.should eq({"bar", "1.0"})
end

it "duplicate properties uses first one" do
File.write("shard.yml", "name: bar\nversion: 1.0\nname: foo\nversion: foo")
ProjectInfo.read_shard_properties.should eq({"bar", "1.0"})
end

it "strip whitespace" do
File.write("shard.yml", "name: bar \nversion: 1.0 ")
ProjectInfo.read_shard_properties.should eq({"bar", "1.0"})
end

it "strip quotes" do
File.write("shard.yml", "name: 'bar'\nversion: '1.0'")
ProjectInfo.read_shard_properties.should eq({"bar", "1.0"})
end

it "ignores comments" do
File.write("shard.yml", "name: bar # comment\nversion: 1.0 # comment")
ProjectInfo.read_shard_properties.should eq({"bar", "1.0"})

File.write("shard.yml", "name: # comment\nversion: # comment")
ProjectInfo.read_shard_properties.should eq({nil, nil})
end
end
end
25 changes: 24 additions & 1 deletion src/compiler/crystal/command/docs.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Crystal::Command
sitemap_base_url = nil
sitemap_priority = "1.0"
sitemap_changefreq = "never"
project_info = Doc::ProjectInfo.new

compiler = Compiler.new

Expand All @@ -24,6 +25,14 @@ class Crystal::Command
Options:
BANNER

opts.on("--project-name=NAME", "Set project name") do |value|
project_info.name = value
end

opts.on("--project-version=VERSION", "Set project version") do |value|
project_info.version = value
end

opts.on("--output=DIR", "-o DIR", "Set the output directory (default: #{output_directory})") do |value|
output_directory = value
end
Expand Down Expand Up @@ -88,6 +97,20 @@ class Crystal::Command
setup_compiler_warning_options(opts, compiler)
end

project_info.fill_with_defaults

unless project_info.name?
STDERR.puts "Couldn't determine name from shard.yml, please provide --project-name option"
end

unless project_info.version?
STDERR.puts "Couldn't determine version from git or shard.yml, please provide --project-version option"
end

unless project_info.name? && project_info.version?
abort
end

if options.empty?
sources = [Compiler::Source.new("require", %(require "./src/**"))]
included_dirs = [] of String
Expand All @@ -103,7 +126,7 @@ class Crystal::Command
compiler.wants_doc = true
result = compiler.top_level_semantic sources

Doc::Generator.new(result.program, included_dirs, output_directory, output_format, sitemap_base_url, sitemap_priority, sitemap_changefreq).run
Doc::Generator.new(result.program, included_dirs, output_directory, output_format, sitemap_base_url, sitemap_priority, sitemap_changefreq, project_info).run

report_warnings result
exit 1 if warnings_fail_on_exit?(result)
Expand Down
10 changes: 6 additions & 4 deletions src/compiler/crystal/tools/doc/generator.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Crystal::Doc::Generator
property is_crystal_repo : Bool
@repository : String? = nil
getter repository_name = ""
getter project_info

# Adding a flag and associated css class will add support in parser
FLAG_COLORS = {
Expand All @@ -29,13 +30,14 @@ class Crystal::Doc::Generator
}

def self.new(program : Program, included_dirs : Array(String))
new(program, included_dirs, ".", "html", nil, "1.0", "never")
new(program, included_dirs, ".", "html", nil, "1.0", "never", ProjectInfo.new("test", "0.0.0-test"))
end

def initialize(@program : Program, @included_dirs : Array(String),
@output_dir : String, @output_format : String,
@sitemap_base_url : String?,
@sitemap_priority : String, @sitemap_changefreq : String)
@sitemap_priority : String, @sitemap_changefreq : String,
@project_info : ProjectInfo)
@base_dir = Dir.current.chomp
@types = {} of Crystal::Type => Doc::Type
@repo_name = ""
Expand Down Expand Up @@ -95,7 +97,7 @@ class Crystal::Doc::Generator
raw_body = read_readme
body = doc(program_type, raw_body)

File.write File.join(@output_dir, "index.html"), MainTemplate.new(body, types, repository_name)
File.write File.join(@output_dir, "index.html"), MainTemplate.new(body, types, project_info)

main_index = Main.new(raw_body, Type.new(self, @program), repository_name)
File.write File.join(@output_dir, "index.json"), main_index
Expand Down Expand Up @@ -124,7 +126,7 @@ class Crystal::Doc::Generator
filename = File.join(dir, "#{type.name}.html")
end

File.write filename, TypeTemplate.new(type, all_types)
File.write filename, TypeTemplate.new(type, all_types, project_info)

next if type.program?

Expand Down
12 changes: 12 additions & 0 deletions src/compiler/crystal/tools/doc/html/_sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
<input type="search" class="search-input" placeholder="Search..." spellcheck="false" aria-label="Search">
</div>

<div class="project-summary">
<h1 class="project-name">
<a href="<%= current_type.try(&.path_to("")) %>index.html">
<%= project_info.name %>
</a>
</h1>

<span class="project-version">
<%= project_info.version %>
</span>
</div>

<div class="repository-links">
<a href="<%= current_type.try(&.path_to("")) %>index.html">README</a>
</div>
Expand Down
20 changes: 19 additions & 1 deletion src/compiler/crystal/tools/doc/html/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ body {
}

.sidebar .search-box {
padding: 8px 9px;
padding: 13px 9px;
}

.sidebar input {
Expand Down Expand Up @@ -106,6 +106,24 @@ body {
text-indent: 2px;
}

.project-summary {
display: inline-block;
padding: 9px 15px 30px 30px;
text-align: right;
}

.project-name {
font-size: 1.4rem;
margin: 0;
color: #f4f4f4;
font-weight: 600;
}

.project-version {
margin-top: 5px;
display: block;
}

.sidebar ul {
margin: 0;
padding: 0;
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/crystal/tools/doc/html/js/_search.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,11 @@ CrystalDoc.displaySearchResults = function(results, query) {
CrystalDoc.toggleResultsList = function(visible) {
if (visible) {
document.querySelector(".types-list").classList.add("hidden");
document.querySelector(".repository-links").classList.add("hidden");
document.querySelector(".search-results").classList.remove("hidden");
} else {
document.querySelector(".types-list").classList.remove("hidden");
document.querySelector(".repository-links").classList.remove("hidden");
document.querySelector(".search-results").classList.add("hidden");
}
};
Expand Down
Loading

0 comments on commit f7dd352

Please sign in to comment.