Skip to content

Commit

Permalink
Merge pull request #731 from rubytoolbox/co-readme-display
Browse files Browse the repository at this point in the history
README display
  • Loading branch information
colszowka committed Sep 30, 2020
2 parents 0382c00 + 1567a57 commit 417e8b5
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 3 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Expand Up @@ -61,6 +61,9 @@ gem "http"

gem "sidekiq"

gem "sanitize"
gem "truncato"

gem "redcarpet"
gem "slim-rails"

Expand Down
12 changes: 12 additions & 0 deletions Gemfile.lock
Expand Up @@ -144,6 +144,7 @@ GEM
ruby_parser (~> 3.10)
hashdiff (1.0.1)
high_voltage (3.1.2)
htmlentities (4.3.4)
http (4.4.1)
addressable (~> 2.3)
http-cookie (~> 1.0)
Expand Down Expand Up @@ -199,6 +200,8 @@ GEM
nio4r (2.5.3)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
nokogumbo (2.0.2)
nokogiri (~> 1.8, >= 1.8.4)
notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
Expand Down Expand Up @@ -330,6 +333,10 @@ GEM
ruby_parser (3.14.1)
sexp_processor (~> 4.9)
rubyzip (2.3.0)
sanitize (5.2.1)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
nokogumbo (~> 2.0)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
Expand Down Expand Up @@ -379,6 +386,9 @@ GEM
thread_safe (0.3.6)
tilt (2.0.10)
timecop (0.9.1)
truncato (0.7.11)
htmlentities (~> 4.3.1)
nokogiri (>= 1.7.0, <= 2.0)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
Expand Down Expand Up @@ -470,6 +480,7 @@ DEPENDENCIES
rubocop-performance
rubocop-rails
rubocop-rspec
sanitize
sass-rails (~> 5.0)
selenium-webdriver
sidekiq
Expand All @@ -478,6 +489,7 @@ DEPENDENCIES
spring
spring-watcher-listen (~> 2.0.0)
timecop
truncato
turbolinks (~> 5)
tzinfo-data
uglifier (>= 1.3.0)
Expand Down
16 changes: 16 additions & 0 deletions app/assets/stylesheets/components/readme.sass
@@ -0,0 +1,16 @@
.readme
.label
@extend .column, .is-full, .is-2-widescreen
span
@extend .is-size-5
position: sticky
top: 90px
padding-bottom: 12px
border-bottom: 3px solid $primary
margin-bottom: 24px

i.fa
@extend .has-text-grey-light

.content
@extend .column
4 changes: 4 additions & 0 deletions app/helpers/component_helpers.rb
Expand Up @@ -55,6 +55,10 @@ def project_health_tag(health_status)
render "components/project_health_tag", status: health_status
end

def project_readme(readme)
render "components/project/readme", readme: readme
end

def small_health_indicator(project)
render "components/small_health_indicator", project: project
end
Expand Down
56 changes: 56 additions & 0 deletions app/models/github/readme.rb
@@ -1,11 +1,67 @@
# frozen_string_literal: true

class Github::Readme < ApplicationRecord
module Scrubber
class << self
#
# Sanitizes given html, drops named anchors and adjusts relative
# links to be based on given base_url (if passed, otherwise that step
# is skipped)
#
def scrub(html, base_url: nil)
return if html.blank?

sanitized = Sanitize.fragment(html, Sanitize::Config::RELAXED)

fix_links sanitized, base_url: base_url
end

private

def fix_links(sanitized, base_url:)
doc = Nokogiri::HTML.fragment sanitized

doc.css("a[href]").each do |a|
adjust_link a, base_url: base_url
end

doc.to_s
end

def adjust_link(link, base_url:)
href = link["href"]

# Scrub links to named anchors
if href.start_with?("#")
link.replace link.inner_html
# Relative links get aligned to base_url, depending on whether
# it's an absolute or relative path
elsif base_url && href.exclude?("://")
link["href"] = if href.start_with?("/")
URI.join base_url, href
else
File.join base_url, href
end
end
end
end
end

self.primary_key = :path
self.table_name = :github_readmes

belongs_to :github_repo,
primary_key: :path,
foreign_key: :path,
inverse_of: :readme

def html=(html)
super Scrubber.scrub(html, base_url: github_repo&.blob_url)
end

def truncated_html(limit: 2000)
return if html.blank?

Truncato.truncate(html, max_length: limit)
end
end
6 changes: 6 additions & 0 deletions app/models/github_repo.rb
Expand Up @@ -45,6 +45,12 @@ def url
File.join "https://github.com", path
end

def blob_url
return unless default_branch

File.join url, "blob", default_branch
end

def issues_url
File.join(url, "issues") if has_issues?
end
Expand Down
5 changes: 4 additions & 1 deletion app/models/project.rb
Expand Up @@ -122,12 +122,15 @@ def self.search(query, order: Project::Order.new(directions: Project::Order::SEA
:pull_request_acceptance_rate,
:average_recent_committed_at,
:sibling_gem_with_most_downloads,
:readme,
to: :github_repo,
allow_nil: true,
prefix: :github_repo

def self.find_for_show!(permalink)
includes_associations.find(Github.normalize_path(permalink))
includes_associations
.includes(github_repo: :readme)
.find Github.normalize_path(permalink)
end

def permalink=(permalink)
Expand Down
9 changes: 9 additions & 0 deletions app/views/components/project/_readme.html.slim
@@ -0,0 +1,9 @@
- if readme
section.readme.section: .container: .columns.is-multiline
.label
span
i.fa.fa-book &nbsp;
strong Project Readme

.content
= readme.html.html_safe
12 changes: 12 additions & 0 deletions app/views/pages/components/project_readme.html.slim
@@ -0,0 +1,12 @@
.hero
section.section: .container
p.heading= link_to "Ruby Toolbox UI Components Styleguide", "/pages/components"
h2= current_page.split("/").last.humanize

- readme = Github::Readme.new(html: "<h1>Hello World!</h1><p>Hello World</p>")

= component_example "Project README" do
= project_readme readme

= component_example "Project README is not present" do
= project_readme nil
3 changes: 1 addition & 2 deletions app/views/projects/show.html.slim
Expand Up @@ -21,7 +21,6 @@

.columns: .column= project_health_tags @project


.columns: .links.column
= project_links @project

Expand All @@ -38,4 +37,4 @@
section.section: .container: .project
= project_metrics @project, expanded_view: true

section.section: .container
= project_readme @project.github_repo_readme
23 changes: 23 additions & 0 deletions spec/features/project_spec.rb
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe "Project Display", type: :feature do
let(:project) do
Factories.project "widgets"
end

it "can display Project README" do
visit project_path(project)
expect(page).not_to have_selector(".readme")

project.github_repo.create_readme! html: "<strong>some content</strong>", etag: "1234"
visit project_path(project)

expect(page).to have_selector(".readme")

within ".readme" do
expect(page).to have_text("some content")
end
end
end
82 changes: 82 additions & 0 deletions spec/models/github/readme_spec.rb
@@ -0,0 +1,82 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Github::Readme, type: :model do
let(:repo) do
GithubRepo.new(
path: "foo/bar",
default_branch: "main"
)
end

let(:model) do
described_class.new(
html: "<p>Hello World</p>",
etag: "123123",
github_repo: repo
)
end

describe "html=" do
it "passes the input through the scrubber" do
base_url = "https://example.com/foo"
allow(model.github_repo).to receive(:blob_url).and_return(base_url)
allow(Github::Readme::Scrubber).to receive(:scrub)
.with("input html", base_url: base_url)
.and_return("scrubbed")

model.html = "input html"

expect(model.html).to be == "scrubbed"
end
end

describe "#truncated_html" do
it "is nil when html is empty" do
model.html = " "
expect(model.truncated_html).to be nil
end

it "returns truncated html" do
model.html = '<a href="https://example.com">Hello</a><p>More</p>'
expect(model.truncated_html(limit: 40)).to be == "<a href='https://example.com'>Hello</a><p>...</p>"
end
end

describe Github::Readme::Scrubber do
describe ".scrub" do
it "returns nil if html is blank" do
expect(described_class.scrub(" \n ")).to be nil
end

it "cleans weird html content" do
html = %q{<a href="/" onclick="alert('lol');">Hello</a>}
expect(described_class.scrub(html)).to be == '<a href="/">Hello</a>'
end

it "removes links to named anchors" do
html = '<p><a href="#foobar">Hello</a></p>'
expect(described_class.scrub(html)).to be == "<p>Hello</p>"
end

# rubocop:disable RSpec/ExampleLength
it "exchanges relative links with base url when given" do
html = <<~HTML
<p><a href="https://example.com">Unchanged</a></p>
<p><a href="foo/relative">Changed</a></p>
<p><a href="/absolute">Changed too</a></p>
HTML

expected = <<~HTML
<p><a href="https://example.com">Unchanged</a></p>
<p><a href="https://example.com/subpath/foo/relative">Changed</a></p>
<p><a href="https://example.com/absolute">Changed too</a></p>
HTML

expect(described_class.scrub(html, base_url: "https://example.com/subpath")).to be == expected
end
# rubocop:enable RSpec/ExampleLength
end
end
end
10 changes: 10 additions & 0 deletions spec/models/github_repo_spec.rb
Expand Up @@ -55,6 +55,16 @@ def create_repo!(path:, updated_at: 1.day.ago)
end
end

describe "#blob_url" do
it "is derived from the repo path and default_branch" do
expect(described_class.new(path: "foo/bar", default_branch: "main").blob_url).to be == "https://github.com/foo/bar/blob/main"
end

it "is nil when there is no default_branch" do
expect(described_class.new(path: "foo/bar", default_branch: nil).blob_url).to be nil
end
end

describe "#wiki_url" do
it "is nil when has_wiki is false" do
expect(described_class.new(has_wiki: false).wiki_url).to be_nil
Expand Down
13 changes: 13 additions & 0 deletions spec/models/project_spec.rb
Expand Up @@ -26,6 +26,19 @@
end
end

describe ".find_for_show!" do
let(:project) do
Factories.project "sample"
end

it "eager-loads readme if present" do
project.github_repo.create_readme! html: "hello world", etag: "1234"

found_instance = described_class.find_for_show!(project.permalink)
expect { found_instance.github_repo_readme }.not_to make_database_queries
end
end

describe ".with_bugfix_forks" do
before do
Factories.project "regular"
Expand Down

0 comments on commit 417e8b5

Please sign in to comment.