Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add project name and version to API docs #8792

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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