Compare commits
	
		
			10 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 63ee2ec4ee | |||
| 7b7d303e25 | |||
| 44be5c1249 | |||
| 6ed253c7b9 | |||
| 450944cd0b | |||
| c655b10467 | |||
| 2eb39cbeac | |||
| 66362fc856 | |||
| 8bf41bc242 | |||
| 7dc82fef8b | 
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							@@ -28,3 +28,17 @@ By default, Dokku will proxy HTTP on port 5000.
 | 
			
		||||
dokku apps:create teafolio
 | 
			
		||||
dokku storage:mount teafolio /srv/teafolio-dokku/config.toml:/app/config.toml
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## CHANGELOG
 | 
			
		||||
 | 
			
		||||
2020-05-24 v1.1.0
 | 
			
		||||
- Support limiting the number of concurrent API requests to Gitea
 | 
			
		||||
- Display total number of projects
 | 
			
		||||
- Fix cosmetic issues with page background image, page height, and margins around thumbnails
 | 
			
		||||
 | 
			
		||||
2020-05-24 v1.0.1
 | 
			
		||||
- Remove image dependency from static files
 | 
			
		||||
- Fix a cosmetic issue with `h2`/`h3` margins
 | 
			
		||||
 | 
			
		||||
2020-05-05 v1.0.0
 | 
			
		||||
- Initial release
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										87
									
								
								api.go
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								api.go
									
									
									
									
									
								
							@@ -2,6 +2,7 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
@@ -45,8 +46,16 @@ type MarkdownRequest struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// repos gets a list of Git repositories in this organisation.
 | 
			
		||||
func (this *Application) repos() ([]Repo, error) {
 | 
			
		||||
	resp, err := http.Get(this.cfg.Gitea.URL + `api/v1/orgs/` + url.PathEscape(this.cfg.Gitea.Org) + `/repos`)
 | 
			
		||||
func (this *Application) repos(ctx context.Context) ([]Repo, error) {
 | 
			
		||||
	this.apiSem.Acquire(ctx, 1)
 | 
			
		||||
	defer this.apiSem.Release(1)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, this.cfg.Gitea.URL+`api/v1/orgs/`+url.PathEscape(this.cfg.Gitea.Org)+`/repos`, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -67,8 +76,16 @@ func (this *Application) repos() ([]Repo, error) {
 | 
			
		||||
 | 
			
		||||
// repoFile gets a single file from the default branch of the git repository
 | 
			
		||||
// Usually the default branch is `master`.
 | 
			
		||||
func (this *Application) repoFile(repo, filename string) ([]byte, error) {
 | 
			
		||||
	resp, err := http.Get(this.cfg.Gitea.URL + `api/v1/repos/` + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repo) + `/contents/` + url.PathEscape(filename))
 | 
			
		||||
func (this *Application) repoFile(ctx context.Context, repo, filename string) ([]byte, error) {
 | 
			
		||||
	this.apiSem.Acquire(ctx, 1)
 | 
			
		||||
	defer this.apiSem.Release(1)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, this.cfg.Gitea.URL+`api/v1/repos/`+url.PathEscape(this.cfg.Gitea.Org)+`/`+url.PathEscape(repo)+`/contents/`+url.PathEscape(filename), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -87,8 +104,16 @@ func (this *Application) repoFile(repo, filename string) ([]byte, error) {
 | 
			
		||||
	return cr.Content, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *Application) filesInDirectory(repo, dir string) ([]ReaddirEntry, error) {
 | 
			
		||||
	resp, err := http.Get(this.cfg.Gitea.URL + `api/v1/repos/` + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repo) + `/contents/` + dir) // n.b. $dir param not escaped
 | 
			
		||||
func (this *Application) filesInDirectory(ctx context.Context, repo, dir string) ([]ReaddirEntry, error) {
 | 
			
		||||
	this.apiSem.Acquire(ctx, 1)
 | 
			
		||||
	defer this.apiSem.Release(1)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, this.cfg.Gitea.URL+`api/v1/repos/`+url.PathEscape(this.cfg.Gitea.Org)+`/`+url.PathEscape(repo)+`/contents/`+dir, nil) // n.b. $dir param not escaped
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -121,13 +146,13 @@ func (this *Application) filesInDirectory(repo, dir string) ([]ReaddirEntry, err
 | 
			
		||||
 | 
			
		||||
// imageFilesForRepo finds documentation images for the repository.
 | 
			
		||||
// It searches the dist/ and doc/ subdirectories.
 | 
			
		||||
func (this *Application) imageFilesForRepo(repo string) ([]ReaddirEntry, error) {
 | 
			
		||||
func (this *Application) imageFilesForRepo(ctx context.Context, repo string) ([]ReaddirEntry, error) {
 | 
			
		||||
 | 
			
		||||
	ret := []ReaddirEntry{}
 | 
			
		||||
 | 
			
		||||
	for _, dirName := range []string{`dist`, `doc`} {
 | 
			
		||||
 | 
			
		||||
		files, err := this.filesInDirectory(repo, dirName)
 | 
			
		||||
		files, err := this.filesInDirectory(ctx, repo, dirName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("readdir(%s): %w", dirName, err)
 | 
			
		||||
		}
 | 
			
		||||
@@ -142,8 +167,16 @@ func (this *Application) imageFilesForRepo(repo string) ([]ReaddirEntry, error)
 | 
			
		||||
	return ret, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *Application) topicsForRepo(repo string) ([]string, error) {
 | 
			
		||||
	resp, err := http.Get(this.cfg.Gitea.URL + `api/v1/repos/` + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repo) + `/topics`)
 | 
			
		||||
func (this *Application) topicsForRepo(ctx context.Context, repo string) ([]string, error) {
 | 
			
		||||
	this.apiSem.Acquire(ctx, 1)
 | 
			
		||||
	defer this.apiSem.Release(1)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, this.cfg.Gitea.URL+`api/v1/repos/`+url.PathEscape(this.cfg.Gitea.Org)+`/`+url.PathEscape(repo)+`/topics`, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -163,19 +196,27 @@ func (this *Application) topicsForRepo(repo string) ([]string, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// renderMarkdown calls the remote Gitea server's own markdown renderer.
 | 
			
		||||
func (this *Application) renderMarkdown(repoName string, body string) ([]byte, error) {
 | 
			
		||||
	req := MarkdownRequest{
 | 
			
		||||
func (this *Application) renderMarkdown(ctx context.Context, repoName string, body string) ([]byte, error) {
 | 
			
		||||
	this.apiSem.Acquire(ctx, 1)
 | 
			
		||||
	defer this.apiSem.Release(1)
 | 
			
		||||
 | 
			
		||||
	jb, err := json.Marshal(MarkdownRequest{
 | 
			
		||||
		Context: this.cfg.Gitea.URL + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repoName) + `/src/branch/master`,
 | 
			
		||||
		Mode:    "gfm", // magic constant - Github Flavoured Markdown
 | 
			
		||||
		Text:    body,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	jb, err := json.Marshal(req)
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.Post(this.cfg.Gitea.URL+`api/v1/markdown/`, `application/json`, bytes.NewReader(jb))
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, this.cfg.Gitea.URL+`api/v1/markdown/`, bytes.NewReader(jb))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set(`Content-Type`, `application/json`)
 | 
			
		||||
	req.Header.Set(`Content-Length`, fmt.Sprintf("%d", len(jb)))
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -189,8 +230,18 @@ func (this *Application) renderMarkdown(repoName string, body string) ([]byte, e
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// renderMarkdownRaw calls the remote Gitea server's own markdown renderer.
 | 
			
		||||
func (this *Application) renderMarkdownRaw(body []byte) ([]byte, error) {
 | 
			
		||||
	resp, err := http.Post(this.cfg.Gitea.URL+`api/v1/markdown/raw`, `text/plain`, bytes.NewReader(body))
 | 
			
		||||
func (this *Application) renderMarkdownRaw(ctx context.Context, body []byte) ([]byte, error) {
 | 
			
		||||
	this.apiSem.Acquire(ctx, 1)
 | 
			
		||||
	defer this.apiSem.Release(1)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, this.cfg.Gitea.URL+`api/v1/markdown/raw`, bytes.NewReader(body))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set(`Content-Type`, `text/plain`)
 | 
			
		||||
	req.Header.Set(`Content-Length`, fmt.Sprintf("%d", len(body)))
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ BindTo="0.0.0.0:5656"
 | 
			
		||||
[Gitea]
 | 
			
		||||
URL="https://gitea.com/"
 | 
			
		||||
Org="gitea"
 | 
			
		||||
MaxConnections=2 # Use zero for unlimited
 | 
			
		||||
 | 
			
		||||
[Redirect]
 | 
			
		||||
"old-project-name" = "new-project-name"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								go.mod
									
									
									
									
									
								
							@@ -2,4 +2,7 @@ module teafolio
 | 
			
		||||
 | 
			
		||||
go 1.13
 | 
			
		||||
 | 
			
		||||
require github.com/BurntSushi/toml v0.3.1
 | 
			
		||||
require (
 | 
			
		||||
	github.com/BurntSushi/toml v0.3.1
 | 
			
		||||
	golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,2 +1,4 @@
 | 
			
		||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 | 
			
		||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
			
		||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
 | 
			
		||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								main.go
									
									
									
									
									
								
							@@ -14,12 +14,14 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/BurntSushi/toml"
 | 
			
		||||
	"golang.org/x/sync/semaphore"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	BindTo string
 | 
			
		||||
	Gitea  struct {
 | 
			
		||||
		URL, Org string
 | 
			
		||||
		URL, Org       string
 | 
			
		||||
		MaxConnections int64
 | 
			
		||||
	}
 | 
			
		||||
	Redirect map[string]string
 | 
			
		||||
	Template struct {
 | 
			
		||||
@@ -33,6 +35,7 @@ type Application struct {
 | 
			
		||||
	cfg Config
 | 
			
		||||
 | 
			
		||||
	rxRepoPage, rxRepoImage *regexp.Regexp
 | 
			
		||||
	apiSem                  *semaphore.Weighted
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *Application) Templatepage(w http.ResponseWriter, r *http.Request, pageDesc, extraHead string, cb func()) {
 | 
			
		||||
@@ -77,7 +80,9 @@ func (this *Application) internalError(w http.ResponseWriter, r *http.Request, e
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *Application) Homepage(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	repos, err := this.repos()
 | 
			
		||||
	ctx := r.Context()
 | 
			
		||||
 | 
			
		||||
	repos, err := this.repos(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.internalError(w, r, fmt.Errorf("listing repos: %w", err))
 | 
			
		||||
		return
 | 
			
		||||
@@ -85,7 +90,7 @@ func (this *Application) Homepage(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	topics := make(map[string][]string)
 | 
			
		||||
	for _, repo := range repos {
 | 
			
		||||
		if t, err := this.topicsForRepo(repo.Name); err == nil {
 | 
			
		||||
		if t, err := this.topicsForRepo(ctx, repo.Name); err == nil {
 | 
			
		||||
			topics[repo.Name] = t
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -116,7 +121,7 @@ func (this *Application) Homepage(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			<option value="data-sort-mt">Recent updates</option>
 | 
			
		||||
		</select>
 | 
			
		||||
		
 | 
			
		||||
		<h2>Projects</h2>
 | 
			
		||||
		<h2>Projects <small>(`+fmt.Sprintf("%d", len(repos))+`)</small></h2>
 | 
			
		||||
		<table id="projtable-main" class="projtable">
 | 
			
		||||
`)
 | 
			
		||||
		for _, repo := range repos {
 | 
			
		||||
@@ -175,8 +180,9 @@ func (this *Application) Homepage(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *Application) Bannerpage(w http.ResponseWriter, r *http.Request, repoName string) {
 | 
			
		||||
	ctx := r.Context()
 | 
			
		||||
 | 
			
		||||
	images, err := this.imageFilesForRepo(repoName)
 | 
			
		||||
	images, err := this.imageFilesForRepo(ctx, repoName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.internalError(w, r, fmt.Errorf("listing images: %w", err))
 | 
			
		||||
		return
 | 
			
		||||
@@ -193,11 +199,11 @@ func (this *Application) Bannerpage(w http.ResponseWriter, r *http.Request, repo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *Application) Repopage(w http.ResponseWriter, r *http.Request, repoName string) {
 | 
			
		||||
 | 
			
		||||
	ctx := r.Context()
 | 
			
		||||
	repoURL := this.cfg.Gitea.URL + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repoName)
 | 
			
		||||
	extraHead := ""
 | 
			
		||||
 | 
			
		||||
	readme, err := this.repoFile(repoName, `README.md`)
 | 
			
		||||
	readme, err := this.repoFile(ctx, repoName, `README.md`)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.internalError(w, r, fmt.Errorf("loading README.md: %w", err))
 | 
			
		||||
		return
 | 
			
		||||
@@ -217,20 +223,20 @@ func (this *Application) Repopage(w http.ResponseWriter, r *http.Request, repoNa
 | 
			
		||||
		lines = append([]string{lines[0], lines[1], extraBadgesMd, ""}, lines[2:]...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readmeHtml, err := this.renderMarkdown(repoName, strings.Join(lines, "\n"))
 | 
			
		||||
	readmeHtml, err := this.renderMarkdown(ctx, repoName, strings.Join(lines, "\n"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.internalError(w, r, fmt.Errorf("rendering markdown: %w", err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	images, err := this.imageFilesForRepo(repoName)
 | 
			
		||||
	images, err := this.imageFilesForRepo(ctx, repoName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		this.internalError(w, r, fmt.Errorf("listing images: %w", err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If the Git repository contains a top-level go.mod file, allow vanity imports
 | 
			
		||||
	if goMod, err := this.repoFile(repoName, `go.mod`); err == nil {
 | 
			
		||||
	if goMod, err := this.repoFile(ctx, repoName, `go.mod`); err == nil {
 | 
			
		||||
 | 
			
		||||
		// Check the first line should be `module MODULENAME\n`
 | 
			
		||||
		firstLine := bytes.SplitN(goMod, []byte("\n"), 2)[0]
 | 
			
		||||
@@ -384,5 +390,12 @@ func main() {
 | 
			
		||||
		app.cfg.Gitea.URL += `/`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create semaphore
 | 
			
		||||
	if app.cfg.Gitea.MaxConnections == 0 { // unlimited
 | 
			
		||||
		app.apiSem = semaphore.NewWeighted(99999)
 | 
			
		||||
	} else {
 | 
			
		||||
		app.apiSem = semaphore.NewWeighted(app.cfg.Gitea.MaxConnections)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Fatal(http.ListenAndServe(app.cfg.BindTo, &app))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 5.9 KiB  | 
@@ -25,7 +25,7 @@ h1 a {
 | 
			
		||||
h1 a:hover {
 | 
			
		||||
	color:black;
 | 
			
		||||
}
 | 
			
		||||
h1,h2,h3 {
 | 
			
		||||
h1 {
 | 
			
		||||
	margin-top:0;
 | 
			
		||||
}
 | 
			
		||||
.code {
 | 
			
		||||
@@ -48,8 +48,7 @@ h1,h2,h3 {
 | 
			
		||||
 | 
			
		||||
html, body {
 | 
			
		||||
	/* structural */
 | 
			
		||||
	height:100%;
 | 
			
		||||
	min-height:100%;
 | 
			
		||||
	min-height:100vh;
 | 
			
		||||
	margin:0;
 | 
			
		||||
	border:0;
 | 
			
		||||
	padding:0;
 | 
			
		||||
@@ -58,18 +57,23 @@ html, body {
 | 
			
		||||
	font-family:"Helvetica Neue","Segoe UI",Arial,sans-serif;
 | 
			
		||||
	font-size:13px;
 | 
			
		||||
	line-height:1.4;
 | 
			
		||||
	background:#DDD url('greyzz.png');	/* thanks subtlepatterns.com ! */
 | 
			
		||||
	color:#333;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Create background pattern by layering two gradients */
 | 
			
		||||
html {
 | 
			
		||||
	background: repeating-linear-gradient(45deg, #FFF, #f0f0f0 3px, #fff 6px);
 | 
			
		||||
}
 | 
			
		||||
body {
 | 
			
		||||
	background: repeating-linear-gradient(135deg, rgba(255,255,255,0), rgba(255,255,255,255) 3px, rgba(255,255,255,0) 6px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#container {
 | 
			
		||||
	margin:0 auto;
 | 
			
		||||
	width:960px;
 | 
			
		||||
	position:relative;
 | 
			
		||||
	
 | 
			
		||||
	height:auto !important;
 | 
			
		||||
	height:100%; /* oldIE */
 | 
			
		||||
	min-height:100%;
 | 
			
		||||
	min-height:100vh;
 | 
			
		||||
	
 | 
			
		||||
	/* cosmetic */
 | 
			
		||||
	background:white;
 | 
			
		||||
@@ -137,6 +141,9 @@ html, body {
 | 
			
		||||
	float:left;
 | 
			
		||||
	width: 860px; /* 740px full - 60px rhs column - 2px border */
 | 
			
		||||
}
 | 
			
		||||
.projbody_halfw h2:first-child {
 | 
			
		||||
	margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
.projbody_fullw {
 | 
			
		||||
	
 | 
			
		||||
}
 | 
			
		||||
@@ -147,6 +154,13 @@ html, body {
 | 
			
		||||
 | 
			
		||||
/* */
 | 
			
		||||
 | 
			
		||||
img[src*="shields.io"] {
 | 
			
		||||
	width: auto;
 | 
			
		||||
	height: 20px; /* Set default height to avoid reflow flashing */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* */
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width:960px) {
 | 
			
		||||
	
 | 
			
		||||
	#container {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user