Compare commits
118 Commits
Author | SHA1 | Date | |
---|---|---|---|
e04eb117e5 | |||
2cb08a49e0 | |||
04b71a48c4 | |||
72eae517da | |||
847b213fe2 | |||
5079427678 | |||
bd70d6ae41 | |||
61c889b7ca | |||
64c5d52afe | |||
d9d0451313 | |||
aa96521ca7 | |||
146f08dacd | |||
eeaf1c1dbf | |||
ef3f3b6e9b | |||
76341bb409 | |||
be2354c9b8 | |||
4379d3f428 | |||
b4669565e2 | |||
5b696229a1 | |||
52f90c5673 | |||
428303c9fb | |||
4811fcc29e | |||
ff70556d8e | |||
fb20358834 | |||
8a43a8181e | |||
2e9666521b | |||
b75ec5aba8 | |||
657d5ccd31 | |||
5801030e31 | |||
8adadb0be1 | |||
9af6849a82 | |||
650664a95d | |||
6c21f36d77 | |||
378dbec981 | |||
b10e5696e2 | |||
f05f429728 | |||
d9b52860b6 | |||
242f69c731 | |||
1a0191a716 | |||
17f5355557 | |||
20d9ea66e5 | |||
0f6cabe3ee | |||
d3739c2f81 | |||
cfdeee6a46 | |||
4f6ce8695c | |||
39789220a8 | |||
12eb8a2111 | |||
da4cf9b528 | |||
0e561b38f7 | |||
0240db512f | |||
85ef555bfd | |||
11336d3451 | |||
1a350aab89 | |||
e6df007875 | |||
a4d6e7a0b2 | |||
a0c64ad5b2 | |||
08892bd0b8 | |||
5b98cd36cd | |||
662496c214 | |||
1d8ccdb5ad | |||
138a3c3342 | |||
fc28e8c5c8 | |||
d67c7fc926 | |||
8a9b36d3db | |||
deea763923 | |||
8f34df0cbd | |||
87f6da2957 | |||
766fee12d5 | |||
6488bed03c | |||
ea7092ebef | |||
ac79051db4 | |||
1194223ddd | |||
ad76f6fb7a | |||
f39a05ba59 | |||
67f20f4379 | |||
2c857250c0 | |||
931b6e0208 | |||
8ea9ca2b2f | |||
9def31abeb | |||
1878d023bf | |||
fef327ec61 | |||
5ddd86e4aa | |||
87dd96adbd | |||
9de0095c8d | |||
7480da7568 | |||
81c65b3cce | |||
441c05f096 | |||
671573bc2e | |||
17b0bea213 | |||
0ad96058d5 | |||
092efca34d | |||
0303019641 | |||
8efff11ac2 | |||
4a6f8d90d8 | |||
b572d957fc | |||
0f70fc7bf0 | |||
a38b0dbea5 | |||
42f4bca8ba | |||
2f254fd355 | |||
f54c388b1a | |||
ee66a10fc3 | |||
d52f58ecae | |||
376745b825 | |||
1c432afdbf | |||
df731a8171 | |||
503bdbd527 | |||
8b153119ea | |||
de3e1f1210 | |||
e693abc079 | |||
72bf8d9410 | |||
8736cc97b4 | |||
2eac27a439 | |||
bbbfba2e8f | |||
97c06514eb | |||
4c3e285547 | |||
40a86b41c7 | |||
301d81a637 | |||
dc260a27d6 |
BIN
.dist-archive/code_v118.tar.xz
Normal file
BIN
.dist-archive/code_v126.tar.xz
Normal file
BIN
.dist-archive/code_v13.zip
Normal file
BIN
.dist-archive/code_v132.tar.xz
Normal file
BIN
.dist-archive/code_v39.zip
Normal file
BIN
.dist-archive/code_v54.tar.xz
Normal file
BIN
.dist-archive/code_v64.tar.xz
Normal file
BIN
.dist-archive/code_v72.tar.xz
Normal file
BIN
.dist-archive/code_v97.tar.xz
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
_dist/
|
||||||
|
sites/*/data/
|
||||||
|
sites/*/wwwroot/
|
||||||
|
shields_cache/
|
102
README.md
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# codesite
|
||||||
|
|
||||||
|
A static site generator for a portfolio website.
|
||||||
|
|
||||||
|
This script was used to generate the https://code.ivysaur.me/ website between 2013 and 2020, when it was replaced by a [teafolio](https://code.ivysaur.me/teafolio) instance.
|
||||||
|
|
||||||
|
Written in PHP, Bash
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Generates static website, minimising server load and decreasing attack surface compared to dynamic server-side PHP
|
||||||
|
- Automatic thumbnailing and sprite sheet generation
|
||||||
|
- Download file attachments per-project
|
||||||
|
- One-click rebuild, one-click deploy
|
||||||
|
- Parallel generation
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
2020-05-24: v157
|
||||||
|
- Sunset release
|
||||||
|
- Rewrite `codesite` Git repository history for full public release
|
||||||
|
- Feature: Add support for autogenerating codesite projects from a Git repository (`update_git_remote.sh`)
|
||||||
|
- Feature: Support converting a codesite project into a readonly, archive Git repository (`codesite2git`)
|
||||||
|
- Feature: Support bulk pushing `codesite2git`-converted repositories to a Gitea instance
|
||||||
|
|
||||||
|
2018-12-30: v138
|
||||||
|
- (Private release only)
|
||||||
|
- Feature: Support `[git-repository]` tags to link to a custom VCS repository
|
||||||
|
- Feature: Support markdown link syntax
|
||||||
|
- Feature: Support markdown multiline comments that include code syntax highlight tag
|
||||||
|
- Enhancement: Support Go subpackages under `[go-get]` tags
|
||||||
|
- Enhancement: Strip markdown image urls
|
||||||
|
- Enhancement: Trim leading blank lines
|
||||||
|
- Fix a cosmetic issue with favicons and document rendering mode in some versions of IE
|
||||||
|
|
||||||
|
2017-10-28: v132
|
||||||
|
- Fix an issue with not applying project rename redirections in the new URL format
|
||||||
|
- Fix a cosmetic issue with classifying downloads named with numeric strings
|
||||||
|
- Fix a cosmetic issue with truncated sheilds caused by ID collisions
|
||||||
|
- [⬇️ code_v132.tar.xz](.dist-archive/code_v132.tar.xz) *(106.49 KiB)*
|
||||||
|
|
||||||
|
2017-04-23: v126
|
||||||
|
- Breaking: BASEURL is now a mandatory field
|
||||||
|
- Feature: Support `[go-get]` tags
|
||||||
|
- Feature: Canonical paths for SEO
|
||||||
|
- Feature: Use extension-less paths by default (old .html paths still supported via redirect)
|
||||||
|
- Feature: Support `[url]` tags
|
||||||
|
- Enhancement: Cache-busting parameters on homepage thumbnails
|
||||||
|
- [⬇️ code_v126.tar.xz](.dist-archive/code_v126.tar.xz) *(106.39 KiB)*
|
||||||
|
|
||||||
|
2016-04-19: v118
|
||||||
|
- Feature: Classify download artefacts by matching CHANGELOG entry
|
||||||
|
- Feature: Allow sorting by lifespan, artefacts, release entries
|
||||||
|
- Feature: Separate site generation code from site data repositories
|
||||||
|
- Enhancement: Update supplied CSS normalize script
|
||||||
|
- Enhancement: Read parallelism from number of CPUs
|
||||||
|
- [⬇️ code_v118.tar.xz](.dist-archive/code_v118.tar.xz) *(103.18 KiB)*
|
||||||
|
|
||||||
|
2015-11-08: v97
|
||||||
|
- Feature: Support BBCode b/i/spoiler/entry tags
|
||||||
|
- Feature: Add more CSS styles to allow per-site customistaion
|
||||||
|
- Feature: Use http://shields.io images where appropriate
|
||||||
|
- Feature: Optional blurbs=off, article_header={string}, shields_prefix=true configuration directives
|
||||||
|
- Fix an issue with project update-time detection
|
||||||
|
- Fix a cosmetic issue with whitespace
|
||||||
|
- [⬇️ code_v97.tar.xz](.dist-archive/code_v97.tar.xz) *(107.54 KiB)*
|
||||||
|
|
||||||
|
2015-04-05: v72
|
||||||
|
- Feature: Support redirecting old project names
|
||||||
|
- Feature: Add file hash in download URLs to prevent filename collisions
|
||||||
|
- Fix an issue generating spritesheets even if no project images are present
|
||||||
|
- Don't include `ctime` when estimating project update time
|
||||||
|
- [⬇️ code_v72.tar.xz](.dist-archive/code_v72.tar.xz) *(106.79 KiB)*
|
||||||
|
|
||||||
|
2015-04-05: v64
|
||||||
|
- Feature: Support sorting projects
|
||||||
|
- [⬇️ code_v64.tar.xz](.dist-archive/code_v64.tar.xz) *(106.45 KiB)*
|
||||||
|
|
||||||
|
2015-04-04: v54
|
||||||
|
- Feature: Support multiple code sites
|
||||||
|
- Fix an issue with parallel builds on some versions of windows
|
||||||
|
- Fix an issue corrupting URL links with multiple parameters
|
||||||
|
- Fix a cosmetic issue with page overflow
|
||||||
|
- Fix a cosmetic issue with whitespace on code elements
|
||||||
|
- [⬇️ code_v54.tar.xz](.dist-archive/code_v54.tar.xz) *(105.79 KiB)*
|
||||||
|
|
||||||
|
2014-07-02: v39
|
||||||
|
- Feature: Tags
|
||||||
|
- Feature: Generate pages in parallel
|
||||||
|
- Enhancement: Support raw HTML sections in page content
|
||||||
|
- Fix an issue with URLs containing spaces
|
||||||
|
- Fix a cosmetic issue with image thumbnail backgrounds
|
||||||
|
- Fix a cosmetic issue with download sort order
|
||||||
|
- Fix a cosmetic issue with page layout
|
||||||
|
- [⬇️ code_v39.zip](.dist-archive/code_v39.zip) *(13.11 KiB)*
|
||||||
|
|
||||||
|
2013-09-28: v13
|
||||||
|
- Initial public source code release
|
||||||
|
- [⬇️ code_v13.zip](.dist-archive/code_v13.zip) *(9.02 KiB)*
|
||||||
|
|
||||||
|
2013-09-21: v3
|
||||||
|
- Initial deployment
|
8
TODO.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
- Merge "written in" and "tags"
|
||||||
|
|
||||||
|
- RSS for recent changes
|
||||||
|
|
||||||
|
- RSS for all projects
|
||||||
|
|
||||||
|
- Switchable CSS (reddit theme, 4chan theme, HN theme)
|
27
build-worker
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/lib/bootstrap.php';
|
||||||
|
|
||||||
|
function main($args) {
|
||||||
|
$config = setup_vars();
|
||||||
|
|
||||||
|
$total = $args[0];
|
||||||
|
$pos = $args[1];
|
||||||
|
|
||||||
|
// Perform build tasks
|
||||||
|
|
||||||
|
if ($pos == 0) {
|
||||||
|
buildcommon();
|
||||||
|
if (array_key_exists('redirect', $config)) {
|
||||||
|
buildredirects( $config['redirect'] );
|
||||||
|
}
|
||||||
|
if (array_key_exists('golang-subpackages', $config)) {
|
||||||
|
buildgosubpackages( $config['golang-subpackages'] );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buildprojects($pos, array_decimate(listprojects(), $total, $pos));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main(array_slice($_SERVER['argv'], 1));
|
334
codesite2git
Executable file
@ -0,0 +1,334 @@
|
|||||||
|
#!/usr/bin/php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__.'/lib/bootstrap.php';
|
||||||
|
|
||||||
|
class CProjectGitExporter extends CProject {
|
||||||
|
|
||||||
|
function __construct(string $projdirname, string $projname) {
|
||||||
|
CProject::__construct($projdirname, $projname);
|
||||||
|
|
||||||
|
$this->allowText2Html = false; // Don't produce HTML when processing the download block
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExportAsGit(string $dest) {
|
||||||
|
|
||||||
|
// Create destination directory
|
||||||
|
if (is_dir($dest)) {
|
||||||
|
throw new Exception("Destination path '{$dest}' already exists");
|
||||||
|
}
|
||||||
|
mkdir($dest);
|
||||||
|
shell_exec('cd '. escapeshellarg($dest).' && git init');
|
||||||
|
|
||||||
|
// Parse download information blocks
|
||||||
|
$this->filterLongDescArea();
|
||||||
|
|
||||||
|
// Prepare to start modifying the generated README.md file
|
||||||
|
$this->longdesc = preg_replace('~\\[url=([^\\]]+?)\\](.+?)\\[/url\\]~m', '[\\2](\\1)', $this->longdesc);
|
||||||
|
$this->longdesc = str_replace("\xEF\xBB\xBF", "", $this->longdesc); // Remove interior UTF-8 BOM markers
|
||||||
|
$this->longdesc = str_replace("\r", "", $this->longdesc); // dos2unix
|
||||||
|
|
||||||
|
// Add main header and "written in" badge
|
||||||
|
$lines = explode("\n", $this->longdesc);
|
||||||
|
$header = ["# ".$this->projname, ""];
|
||||||
|
if (strlen($this->subtag) > 0) {
|
||||||
|
$header[] = " . "-blue)";
|
||||||
|
$header[] = "";
|
||||||
|
}
|
||||||
|
$lines = array_merge($header, $lines);
|
||||||
|
|
||||||
|
// Modify some lines to standard markdown format:
|
||||||
|
foreach($lines as $i => $line) {
|
||||||
|
|
||||||
|
// Convert ==HEADERS== to ## Headers
|
||||||
|
if (strlen($line) > 0 && $line[0] == '=') {
|
||||||
|
$tmp = rtrim($line, '=');
|
||||||
|
$indent = substr_count($tmp, '=');
|
||||||
|
|
||||||
|
// Normalise the capitalisation for header text
|
||||||
|
$header_text = substr($tmp, $indent);
|
||||||
|
if ($header_text != "TODO" && $header_text != "FIXME" && $header_text != "WARNING" && $header_text != "API") {
|
||||||
|
$header_text = ucwords(strtolower($header_text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// raise markdown-equivalent header level by 1 to compensate for new header
|
||||||
|
$lines[$i] = $line = str_repeat('#', $indent + 1).' '.$header_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert multiline single-backtick to multi-backtick
|
||||||
|
// Heuristic: if there is only a single backtick in the line
|
||||||
|
if (substr_count($line, '`') == 1) {
|
||||||
|
$lines[$i] = $line = str_replace('`', "\n```\n", $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the previous line started with a hyphen, and this line didn't, we need an
|
||||||
|
// extra line separator
|
||||||
|
if ($i > 0 && strlen($line) > 0 &&
|
||||||
|
$line[0] != '-' && strlen($lines[$i-1]) > 0 && $lines[$i-1][0] == '-'
|
||||||
|
) {
|
||||||
|
$lines[$i] = $line = "\n".$line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all images to a doc/ subdirectory
|
||||||
|
if (count($this->images) > 0) {
|
||||||
|
mkdir($dest.'/doc');
|
||||||
|
foreach($this->images as $img) {
|
||||||
|
// Normalise some legacy file extension variants while we're here
|
||||||
|
copy($this->dir.$img, $dest.'/doc/'.str_replace(['.jpeg', '.JPG', '.JPEG'], ['.jpg', '.jpg', '.jpg'], $img));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all downloads to a dist-archive/ subdirectory
|
||||||
|
if (count($this->downloads) > 0) {
|
||||||
|
|
||||||
|
mkdir($dest.'/dist-archive');
|
||||||
|
foreach($this->downloads as $file) {
|
||||||
|
copy($this->dir.$file, $dest.'/dist-archive/'.$file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->downloads_section_was_replaced) {
|
||||||
|
// Add our own downloads section
|
||||||
|
$lines[] = $this->renderDownloadsBlock($this->downloads, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save final README
|
||||||
|
file_put_contents($dest.'/README.md', implode("\n", $lines));
|
||||||
|
|
||||||
|
// Extra properties file
|
||||||
|
$ctime = $this->lastupdate - ($this->lifespan * 3600); // Lifespans are measured in hours
|
||||||
|
|
||||||
|
if (strlen($this->shortdesc) > 0) {
|
||||||
|
$this->shortdesc = rtrim($this->shortdesc, '.').'.';
|
||||||
|
$this->shortdesc[0] = strtoupper($this->shortdesc[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($dest.'/.legacy-codesite.toml', "# Converted with codesite2git
|
||||||
|
|
||||||
|
project_name=".json_encode($this->projname)."
|
||||||
|
short_description=".json_encode($this->shortdesc)."
|
||||||
|
written_in_lang=".json_encode($this->subtag)."
|
||||||
|
topics=".json_encode($this->tags)."
|
||||||
|
ctime=".$ctime."
|
||||||
|
mtime=".$this->lastupdate."
|
||||||
|
|
||||||
|
");
|
||||||
|
|
||||||
|
// Git commit everything
|
||||||
|
// Once for the meta with ctime; once for all files with the mtime
|
||||||
|
$command = (
|
||||||
|
'cd '. escapeshellarg($dest).' && '.
|
||||||
|
'git add .legacy-codesite.toml ; '.
|
||||||
|
'GIT_COMMITTER_DATE="'.date(DATE_ISO8601, $ctime).'" git commit -m "initial meta commit" --date '.escapeshellarg(date(DATE_ISO8601, $ctime)).' ; '.
|
||||||
|
'git add -A ; '.
|
||||||
|
'GIT_COMMITTER_DATE="'.date(DATE_ISO8601, $this->lastupdate).'" git commit -m "commit all archived files" --date '.escapeshellarg(date(DATE_ISO8601, $this->lastupdate))
|
||||||
|
);
|
||||||
|
//echo $command."\n";
|
||||||
|
shell_exec($command);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderDownloadsBlock($render_downloads, $include_header=false) { // override
|
||||||
|
if (! count($render_downloads)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ret = "\n";
|
||||||
|
if ($include_header) {
|
||||||
|
$ret .= "## Download\n\n";
|
||||||
|
}
|
||||||
|
foreach($render_downloads as $filename) {
|
||||||
|
// It's possible to make a general URL that links directly to the raw download endpoint
|
||||||
|
// But because our markdown is rendered in context of the repo, we can't properly make a relative link
|
||||||
|
// We could make an absolute link, but that's probably a bad idea w.r.t. any future Git migrations
|
||||||
|
// Just link to the file in-repo although it won't be a RAW download link
|
||||||
|
|
||||||
|
$ret .= "- [⬇️ {$filename}](dist-archive/".rawurlencode($filename).") "; // raw/branch/master/dist-archive/{$filename}
|
||||||
|
$ret .= "*(".fbytes(filesize($this->dir.$filename)).")*\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gitea {
|
||||||
|
protected $url;
|
||||||
|
protected $org;
|
||||||
|
protected $token;
|
||||||
|
|
||||||
|
public function __construct(string $url, string $org, string $token) {
|
||||||
|
$this->url = $url;
|
||||||
|
$this->org = $org;
|
||||||
|
$this->token = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function repoExists(string $repo): bool {
|
||||||
|
|
||||||
|
$resp = @file_get_contents($this->url.'api/v1/repos/'.$this->org.'/'.$repo);
|
||||||
|
if ($resp === false) {
|
||||||
|
return false; // not conclusive but it will suffice
|
||||||
|
}
|
||||||
|
if (strpos($resp, 'does not exist')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inf = json_decode($resp, true);
|
||||||
|
if (is_array($inf) && $inf['name'] == $repo) {
|
||||||
|
return true; // definitely yes
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception("Couldn't decide whether repo {$repo} exists\n\n".var_export($resp, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createRepo(string $repo) {
|
||||||
|
$command = ('curl -sS -X POST ' .
|
||||||
|
escapeshellarg($this->url.'api/v1/admin/users/'.$this->org.'/repos?access_token='.urlencode($this->token)) . ' '.
|
||||||
|
'-H "accept: application/json" -H "Content-Type: application/json" -d ' . escapeshellarg(json_encode(['name' => $repo]))
|
||||||
|
);
|
||||||
|
|
||||||
|
$code = 0;
|
||||||
|
$output = [];
|
||||||
|
exec($command, $output, $code);
|
||||||
|
if ($code == 0) {
|
||||||
|
return; // success
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception("failed to create repository {$repo} (exit code {$code})\n\n".var_export($output, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateRepoProperties(string $repo, string $description) {
|
||||||
|
$json_update = [
|
||||||
|
'archived' => true,
|
||||||
|
'description' => $description,
|
||||||
|
|
||||||
|
// SPECIAL https://code.ivysaur.me/ - don't leave in commit {{{
|
||||||
|
'has_wiki' => false,
|
||||||
|
'website' => 'https://code.ivysaur.me/'.$repo,
|
||||||
|
// }}}
|
||||||
|
];
|
||||||
|
|
||||||
|
$command = ('curl -sS -X PATCH ' .
|
||||||
|
escapeshellarg($this->url.'api/v1/repos/'.$this->org.'/'.$repo.'?access_token='.urlencode($this->token)) . ' '.
|
||||||
|
'-H "accept: application/json" -H "Content-Type: application/json" -d ' . escapeshellarg(json_encode($json_update))
|
||||||
|
);
|
||||||
|
|
||||||
|
$code = 0;
|
||||||
|
$output = [];
|
||||||
|
exec($command, $output, $code);
|
||||||
|
if ($code == 0) {
|
||||||
|
return; // success
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception("failed to update repository {$repo} properties: (exit code {$code})\n\n".var_export($output, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRepoTopics(string $repo, array $topics) {
|
||||||
|
$command = ('curl -sS -X PUT ' .
|
||||||
|
escapeshellarg($this->url.'api/v1/repos/'.$this->org.'/'.$repo.'/topics?access_token='.urlencode($this->token)) . ' '.
|
||||||
|
'-H "accept: application/json" -H "Content-Type: application/json" -d ' . escapeshellarg(json_encode(['topics' => $topics]))
|
||||||
|
);
|
||||||
|
|
||||||
|
$code = 0;
|
||||||
|
$output = [];
|
||||||
|
exec($command, $output, $code);
|
||||||
|
if ($code == 0) {
|
||||||
|
return; // success
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception("failed to update repository {$repo} topics: (exit code {$code})\n\n".var_export($output, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function gitRemoteUrlFor(string $repo): string {
|
||||||
|
return $this->url.$this->org.'/'.$repo.'.git';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function codesite2git(string $projdirname, string $projname, string $dest): CProjectGitExporter {
|
||||||
|
|
||||||
|
// Parse existing project
|
||||||
|
$c = new CProjectGitExporter($projdirname, $projname);
|
||||||
|
// var_dump($c);
|
||||||
|
$c->ExportAsGit($dest);
|
||||||
|
|
||||||
|
return $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
die("Usage:
|
||||||
|
codesite2git [FLAGS...]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--single CODESITE_ROOT_PATH PROJECT_DIR_NAME PROJECT_REAL_NAME DEST_DIR
|
||||||
|
--all CODESITE_ROOT_PATH TEMP_DIR GITEA_URL GITEA_ORG GITEA_AUTH_TOKEN
|
||||||
|
");
|
||||||
|
die(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main(array $argv) {
|
||||||
|
if (count($argv) < 2) {
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($argv[1] == '--single') {
|
||||||
|
if (count($argv) != 6) {
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = setup_vars($argv[2]);
|
||||||
|
codesite2git($argv[3], $argv[4], $argv[5]);
|
||||||
|
|
||||||
|
} else if ($argv[1] == "--all") {
|
||||||
|
if (count($argv) != 7) {
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = setup_vars($argv[2]);
|
||||||
|
$repos = glob(BASEDIR.'data/*');
|
||||||
|
|
||||||
|
$temp_dir = $argv[3];
|
||||||
|
$gitea = new Gitea($argv[4], $argv[5], $argv[6]);
|
||||||
|
|
||||||
|
foreach($repos as $i => $path) {
|
||||||
|
|
||||||
|
$projdir = basename($path);
|
||||||
|
$reponame = explode('-', $projdir, 2)[1];
|
||||||
|
|
||||||
|
echo sprintf("[%3d/%3d] Processing '%s'... ", $i+1, count($repos), $reponame);
|
||||||
|
|
||||||
|
// Convert to git
|
||||||
|
$git_repo_dir = $temp_dir.'/'.$projdir.'-archive.git';
|
||||||
|
$c = codesite2git($projdir, $reponame, $git_repo_dir);
|
||||||
|
|
||||||
|
// Check if Gitea repo already exists
|
||||||
|
if ($gitea->repoExists($reponame)) {
|
||||||
|
throw new \Exception("Abandoning process since repo {$reponame} already exists in Gitea - move it out of the source directory first");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new Gitea repo
|
||||||
|
$gitea->createRepo($reponame);
|
||||||
|
if (! $gitea->repoExists($reponame)) {
|
||||||
|
throw new \Exception("Created repo {$reponame} successfully but it doesn't seem to exist?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add git remote and push
|
||||||
|
shell_exec(
|
||||||
|
'cd '.escapeshellarg($git_repo_dir) .' && '.
|
||||||
|
'git remote add origin '.escapeshellarg($gitea->gitRemoteUrlFor($reponame)).' && '.
|
||||||
|
'git push origin master'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set Gitea topics + description
|
||||||
|
$gitea->updateRepoProperties($reponame, $c->shortdesc);
|
||||||
|
$gitea->setRepoTopics($reponame, $c->tags);
|
||||||
|
|
||||||
|
echo " Done\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main($_SERVER['argv']);
|
@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
|
|
||||||
start C:\path\to\cygwin\bin\mintty.exe /bin/bash -l -c "rsync -avz -e ""ssh -i /cygdrive/c/path/to/deploy_key"" --progress /cygdrive/c/path/to/code/wwwroot www-data@your-web-server:/var/www/path/to/code-site/"
|
|
BIN
doc/screenshot.png
Normal file
After Width: | Height: | Size: 115 KiB |
@ -1,8 +0,0 @@
|
|||||||
|
|
||||||
<p>
|
|
||||||
Homepage blurb goes here.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>PROJECTS</strong>
|
|
||||||
</p>
|
|
408
lib/CProject.php
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class CProject {
|
||||||
|
|
||||||
|
protected $dir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $projname
|
||||||
|
*/
|
||||||
|
public $projname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $shortdesc
|
||||||
|
*/
|
||||||
|
public $shortdesc = '(no description)';
|
||||||
|
public $subtag = '';
|
||||||
|
public $lastupdate = 0;
|
||||||
|
public $numreleases = 0;
|
||||||
|
|
||||||
|
protected $longdesc = '';
|
||||||
|
protected $prefix_html = '';
|
||||||
|
protected $images = array();
|
||||||
|
protected $downloads = array();
|
||||||
|
protected $downloads_hashes = array();
|
||||||
|
|
||||||
|
public $downloads_section_was_replaced = false;
|
||||||
|
|
||||||
|
public $lifespan = 0;
|
||||||
|
|
||||||
|
public $tags = array();
|
||||||
|
|
||||||
|
public $homeimage = null;
|
||||||
|
|
||||||
|
protected $go_get_target = '';
|
||||||
|
protected $git_repo = '';
|
||||||
|
|
||||||
|
protected $allowText2Html = true;
|
||||||
|
|
||||||
|
public function __construct($dirname, $projname) {
|
||||||
|
$this->dir = BASEDIR.'data/'.$dirname.'/';
|
||||||
|
$this->projname = $projname;
|
||||||
|
|
||||||
|
$matches = [];
|
||||||
|
|
||||||
|
// Identify resources in folder
|
||||||
|
|
||||||
|
$ls = scandir($this->dir);
|
||||||
|
$found_real_lastupdate = false;
|
||||||
|
foreach($ls as $file) {
|
||||||
|
if ($file[0] == '.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($file == 'README.txt') {
|
||||||
|
|
||||||
|
$this->longdesc = file_get_contents($this->dir.'README.txt');
|
||||||
|
$this->longdesc = str_replace("\r", "", $this->longdesc); // filter windows CR
|
||||||
|
$this->longdesc = preg_replace("~[\s\t]*$~s", "", $this->longdesc); // filter trailing spaces at line endings
|
||||||
|
|
||||||
|
// Guess 'last update' time
|
||||||
|
if (preg_match('~\n(\d\d\d\d-\d\d-\d\d)~', $this->longdesc, $matches)) {
|
||||||
|
// Use first date entry (assumed to be a CHANGELOG)
|
||||||
|
$this->lastupdate = strtotime($matches[1]);
|
||||||
|
$found_real_lastupdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find number of releases
|
||||||
|
preg_match_all('~\n(\d\d\d\d-\d\d-\d\d)~', $this->longdesc, $matches, PREG_SET_ORDER);
|
||||||
|
$this->numreleases = count($matches);
|
||||||
|
|
||||||
|
// Find support lifespan (newest minus youngest)
|
||||||
|
$eldest = time();
|
||||||
|
$newest = 0;
|
||||||
|
foreach($matches as $match) {
|
||||||
|
$stamp = strtotime($match[1]);
|
||||||
|
$eldest = min($stamp, $eldest);
|
||||||
|
$newest = max($stamp, $newest);
|
||||||
|
}
|
||||||
|
$this->lifespan = floor(max(0, $newest - $eldest) / 3600); // could divide by 86400 but it doesn't matter
|
||||||
|
|
||||||
|
// Find 'written in'
|
||||||
|
if (preg_match('~Written in ([^\\n]+)~', $this->longdesc, $matches)) {
|
||||||
|
$this->subtag = rtrim($matches[1], ' .');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find `go-get` tags
|
||||||
|
$this->longdesc = preg_replace_callback('~\[go-get\](.+)\[/go-get\]~', function($matches) {
|
||||||
|
$this->go_get_target = $matches[1];
|
||||||
|
return '';
|
||||||
|
}, $this->longdesc);
|
||||||
|
|
||||||
|
// Find tags
|
||||||
|
if (preg_match('~Tags: ([^\\n]+)~', $this->longdesc, $matches)) {
|
||||||
|
$this->tags = array_map('trim', explode(',', $matches[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find `entry` tags
|
||||||
|
$this->longdesc = preg_replace(
|
||||||
|
'~\\[entry=([^\\]]+?)\\](.+?)\\[/entry\\]~m',
|
||||||
|
'[url='.BASEURL.'\\1/]\\2[/url]', // hesc still hasn't happened, transform bbcode->bbcode
|
||||||
|
$this->longdesc
|
||||||
|
);
|
||||||
|
|
||||||
|
// Strip out any markdown image links
|
||||||
|
// [](doc/image1.png)
|
||||||
|
$this->longdesc = preg_replace('~\\[!.+?\\)\\]\\(.+?\\)~m', '', $this->longdesc);
|
||||||
|
|
||||||
|
// Find "Written in" tags
|
||||||
|
$this->prefix_html = '';
|
||||||
|
$this->longdesc = preg_replace_callback('~\nWritten in ([^\\n]+)~ms', function($matches) {
|
||||||
|
$this->prefix_html .= (
|
||||||
|
(SHIELDS_PREFIX ? mkshield('build', 'success', 'brightgreen').' ' : '').
|
||||||
|
mkshield('written in', rtrim($matches[1], '.'), 'blue')
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}, $this->longdesc);
|
||||||
|
|
||||||
|
// Find 'git-repository' tags
|
||||||
|
$this->longdesc = preg_replace_callback('~\[git\](.+)\[/git\]~', function($matches) {
|
||||||
|
$this->git_repo = $matches[1];
|
||||||
|
if (strlen($this->prefix_html) > 0) {
|
||||||
|
$this->prefix_html .= ' ';
|
||||||
|
}
|
||||||
|
$this->prefix_html .= '<a href="'.hesc($this->git_repo).'">'.mkshield('vcs', 'git', 'yellowgreen', ['logo' => 'git']).'</a>';
|
||||||
|
return '';
|
||||||
|
}, $this->longdesc);
|
||||||
|
|
||||||
|
// Collapse multiple blank lines
|
||||||
|
$this->longdesc = ltrim($this->longdesc, "\n");
|
||||||
|
while(strpos($this->longdesc, "\n\n\n") !== false) {
|
||||||
|
$this->longdesc = str_replace("\n\n\n", "\n\n", $this->longdesc);
|
||||||
|
}
|
||||||
|
$this->longdesc = rtrim($this->longdesc, "\n")."\n";
|
||||||
|
|
||||||
|
// Extract short description (last)
|
||||||
|
$parts = explode("\n", $this->longdesc);
|
||||||
|
$this->shortdesc = array_shift($parts);
|
||||||
|
$this->shortdesc[0] = strtolower($this->shortdesc[0]); // cosmetic lowercase
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $found_real_lastupdate) {
|
||||||
|
$this->lastupdate = max(
|
||||||
|
$this->lastupdate,
|
||||||
|
// filectime($this->dir.$file),
|
||||||
|
($file == 'README.txt' ? filectime($this->dir.$file) : filemtime($this->dir.$file)) // Don't count README updates
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_image($file)) {
|
||||||
|
$this->images[] = $file;
|
||||||
|
} else {
|
||||||
|
$this->downloads[] = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
natcasesort($this->downloads);
|
||||||
|
$this->downloads = array_reverse($this->downloads);
|
||||||
|
|
||||||
|
for($i = 0, $e = count($this->downloads); $i !== $e; ++$i) {
|
||||||
|
$this->downloads_hashes[] = (
|
||||||
|
sha1_file($this->dir.$this->downloads[$i])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function genHomeImage() {
|
||||||
|
if (count($this->images)) {
|
||||||
|
|
||||||
|
$this->homeimage = mkthumbnail(
|
||||||
|
$this->dir.$this->images[0],
|
||||||
|
null, // raw handle
|
||||||
|
INDEX_THUMB_W, INDEX_THUMB_H
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterLongDescArea() {
|
||||||
|
|
||||||
|
// If *all* downloads can be assigned to a changelog release entry,
|
||||||
|
// then move the files
|
||||||
|
// A release entry is marked by any string following the date field.
|
||||||
|
|
||||||
|
// Add one more NL than we really want, for regex reasons
|
||||||
|
$this->longdesc .= "\n";
|
||||||
|
|
||||||
|
preg_match_all('~^(\d\d\d\d-\d\d-\d\d)\s?:? (.+?)\n\n~ms', $this->longdesc, $matches, PREG_SET_ORDER);
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Ensure changelog exists
|
||||||
|
if (! count($matches)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all tags
|
||||||
|
$known_tags = [];
|
||||||
|
foreach($matches as $i => $match) {
|
||||||
|
$tag = trim(explode("\n", $match[2])[0]);
|
||||||
|
$known_tags[$tag] = $i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all release entries have tags
|
||||||
|
if (count($matches) != count($known_tags)) {
|
||||||
|
error_log("[".$this->projname."] not all release entries have tags\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all downloads can be assigned to tags.
|
||||||
|
// In the event of a download matching multiple tags, it'll
|
||||||
|
// be assigned to the newest (topmost) entry
|
||||||
|
$found_idx = [];
|
||||||
|
$render_per_tag = [];
|
||||||
|
foreach(array_keys($known_tags) as $tagname) {
|
||||||
|
$render_per_tag[$tagname] = [];
|
||||||
|
}
|
||||||
|
foreach($this->downloads as $idx => $filename) {
|
||||||
|
foreach(array_keys($known_tags) as $tagname) {
|
||||||
|
if (stripos($filename, (string)$tagname) !== false) {
|
||||||
|
$found_idx[$idx] = $tagname;
|
||||||
|
|
||||||
|
$render_per_tag[$tagname][$idx] = $filename;
|
||||||
|
break; // next file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count($found_idx) != count($this->downloads)) {
|
||||||
|
error_log("[".$this->projname."] not all downloads have matching tags");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make HTML modifications
|
||||||
|
foreach($known_tags as $tag_name => $tag_idx) {
|
||||||
|
$find = rtrim($matches[$tag_idx][0]);
|
||||||
|
$this->longdesc = str_replace(
|
||||||
|
$find,
|
||||||
|
$find.'${{TAG_'.$tag_idx.'}}',
|
||||||
|
$this->longdesc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->longdesc = substr($this->longdesc, 0, strlen($this->longdesc)-1); // Strip the extra NL we added
|
||||||
|
|
||||||
|
if ($this->allowText2Html) {
|
||||||
|
$this->longdesc = text2html($this->longdesc);
|
||||||
|
}
|
||||||
|
foreach($known_tags as $tag_name => $tag_idx) {
|
||||||
|
$this->longdesc = str_replace(
|
||||||
|
'${{TAG_'.$tag_idx.'}}',
|
||||||
|
$this->renderDownloadsBlock($render_per_tag[$tag_name], false),
|
||||||
|
$this->longdesc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->allowText2Html) {
|
||||||
|
$this->longdesc = str_replace("</ul>\n<br />", "</ul>", $this->longdesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip displaying the global downloads area
|
||||||
|
// This flag also indicates that the content has been pre-HTMLified
|
||||||
|
$this->downloads_section_was_replaced = true;
|
||||||
|
|
||||||
|
// Successful upgrade
|
||||||
|
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
if (! $this->downloads_section_was_replaced) {
|
||||||
|
$this->longdesc = substr($this->longdesc, 0, strlen($this->longdesc)-1); // Strip the extra NL we added
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function numDownloads() {
|
||||||
|
return count($this->downloads);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write() {
|
||||||
|
|
||||||
|
// Generate image thumbnails
|
||||||
|
|
||||||
|
foreach($this->images as $idx => $image) {
|
||||||
|
$outfile = BASEDIR.'wwwroot/img/'.$this->projname.'_'.$idx;
|
||||||
|
copy($this->dir.$image, $outfile.'.'.str_ext($image));
|
||||||
|
|
||||||
|
mkthumbnail($outfile.'.'.str_ext($image), $outfile.'_thumb.jpg', PAGE_THUMB_W, PAGE_THUMB_H);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy downloads to wwwroot
|
||||||
|
|
||||||
|
foreach($this->downloads as $idx => $filename) {
|
||||||
|
$cmkdir = @mkdir( BASEDIR.'wwwroot/srv/'.$this->downloads_hashes[$idx] );
|
||||||
|
|
||||||
|
if (! $cmkdir) {
|
||||||
|
fputs(
|
||||||
|
STDOUT,
|
||||||
|
"WARNING: Couldn't create directory ".$this->downloads_hashes[$idx].
|
||||||
|
" for file '${filename}'".
|
||||||
|
" in project '".$this->projname."'!\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(
|
||||||
|
$this->dir.$filename,
|
||||||
|
BASEDIR.'wwwroot/srv/'.$this->downloads_hashes[$idx].'/'.$filename
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate index page
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$this->index();
|
||||||
|
|
||||||
|
$extra_head_items = [];
|
||||||
|
|
||||||
|
$extra_head_items[] = '<link rel="canonical" href="'.hesc(BASEURL.$this->projname).'">'; // TODO include golang `go get` meta if necessary
|
||||||
|
|
||||||
|
if (strlen($this->go_get_target) > 0) {
|
||||||
|
$extra_head_items[] = '<meta name="go-import" content="'.hesc($this->go_get_target).'">'; // TODO include golang `go get` meta if necessary
|
||||||
|
}
|
||||||
|
|
||||||
|
$idxfile = template($this->projname.' | '.SITE_TITLE, ob_get_clean(), implode("\n", $extra_head_items));
|
||||||
|
mkdir(BASEDIR.'wwwroot/'.$this->projname);
|
||||||
|
|
||||||
|
file_put_contents(BASEDIR.'wwwroot/'.$this->projname.'/index.html', $idxfile); // new URL format
|
||||||
|
file_put_contents(BASEDIR.'wwwroot/'.$this->projname.'.html', redirecthtml(BASEURL.$this->projname)); // old URL format
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassAttr() {
|
||||||
|
if (count($this->tags)) {
|
||||||
|
return 'taggedWith-'.implode(' taggedWith-', $this->tags);
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderDownloadsBlock($render_downloads, $include_header=false) {
|
||||||
|
if (! count($render_downloads)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<?php if ($include_header) { ?>
|
||||||
|
<strong>DOWNLOAD</strong>
|
||||||
|
<?php } ?>
|
||||||
|
<ul class="<?=$include_header ? 'downloads-large' : 'downloads-small' ?>">
|
||||||
|
<?php foreach($render_downloads as $idx => $filename) { ?>
|
||||||
|
<li>
|
||||||
|
<a href="<?=BASEURL?>srv/<?=hesc($this->downloads_hashes[$idx])?>/<?=hesc(rawurlencode($filename))?>"><?=hesc($filename)?></a>
|
||||||
|
<small>
|
||||||
|
<?=hesc(fbytes(filesize(BASEDIR.'wwwroot/srv/'.$this->downloads_hashes[$idx].'/'.$filename)))?>
|
||||||
|
</small>
|
||||||
|
</li>
|
||||||
|
<?php } ?>
|
||||||
|
</ul>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index() {
|
||||||
|
|
||||||
|
$this->filterLongDescArea();
|
||||||
|
$longdesc_html = $this->downloads_section_was_replaced ? $this->longdesc : text2html($this->longdesc);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<h2><?=hesc(str_replace('_', ' ', $this->projname))?></h2>
|
||||||
|
|
||||||
|
<div class="projinfo">
|
||||||
|
|
||||||
|
<div class="projbody projbody_<?=(count($this->images) ? 'half' : 'full')?>w">
|
||||||
|
|
||||||
|
<?php if (strlen($this->prefix_html)) { ?>
|
||||||
|
<p style="margin-top:0;"><?=$this->prefix_html?></p>
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
<strong><?=hesc(strtoupper(ARTICLE_HEADER))?></strong>
|
||||||
|
|
||||||
|
<div class="content-paragraph">
|
||||||
|
<?=$longdesc_html?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?=file_get_contents(BASEDIR.'/footer.htm')?>
|
||||||
|
|
||||||
|
<?php if (! $this->downloads_section_was_replaced) { ?>
|
||||||
|
<?=$this->renderDownloadsBlock($this->downloads, true)?>
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (count($this->images)) { ?>
|
||||||
|
<div class="projimg">
|
||||||
|
<?php foreach($this->images as $idx => $origname) { ?>
|
||||||
|
<a href="<?=BASEURL?>img/<?=hesc(urlencode($this->projname))?>_<?=$idx?>.<?=str_ext($origname)?>"><img src="<?=BASEURL?>img/<?=hesc(urlencode($this->projname))?>_<?=$idx?>_thumb.jpg" class="thumbimage"></a>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="clear:both;"></div>
|
||||||
|
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
47
lib/bootstrap.php
Executable file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
ini_set('display_errors', 'On');
|
||||||
|
date_default_timezone_set('Etc/UTC');
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require __DIR__.'/util.php';
|
||||||
|
require __DIR__.'/template.php';
|
||||||
|
require __DIR__.'/CProject.php';
|
||||||
|
|
||||||
|
define('SHIELDS_CACHE_DIR', __DIR__.'/../shields_cache/');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up global defines for a given codesite project
|
||||||
|
* Should be called once we have the root path for a codesite project.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function setup_vars(string $basedir="./"): array {
|
||||||
|
|
||||||
|
// Parse configuration
|
||||||
|
if (! is_file($basedir.'config.ini')) {
|
||||||
|
die("[FATAL] Non-file '${basedir}config.ini'!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = @parse_ini_file(
|
||||||
|
$basedir . 'config.ini',
|
||||||
|
true,
|
||||||
|
INI_SCANNER_RAW
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($config === false) {
|
||||||
|
die("[FATAL] Couldn't load '${basedir}config.ini'!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
define('BASEDIR', $basedir);
|
||||||
|
define('BASEURL', trim($config['codesite']['baseurl']));
|
||||||
|
define('SITE_TITLE', trim($config['codesite']['title']));
|
||||||
|
define('PAGE_THUMB_W', intval($config['codesite']['page_thumb_w']));
|
||||||
|
define('PAGE_THUMB_H', intval($config['codesite']['page_thumb_h']));
|
||||||
|
define('INDEX_THUMB_W', intval($config['codesite']['index_thumb_w']));
|
||||||
|
define('INDEX_THUMB_H', intval($config['codesite']['index_thumb_h']));
|
||||||
|
define('SHOW_BLURBS', !(isset($config['codesite']['blurbs']) && $config['codesite']['blurbs'] === 'off') );
|
||||||
|
define('ARTICLE_HEADER', (isset($config['codesite']['article_header']) ? $config['codesite']['article_header'] : 'ABOUT') );
|
||||||
|
define('SHIELDS_PREFIX', isset($config['codesite']['shields_prefix']));
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
214
lib/template.php
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function template($title, $content, $extra_head='') {
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><?=hesc($title)?></title>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" >
|
||||||
|
<meta name="viewport" content="width=960" >
|
||||||
|
<?=$extra_head?>
|
||||||
|
<link rel="icon" href="<?=BASEURL?>static/favicon.ico" type="image/x-icon">
|
||||||
|
<link type="text/css" rel="stylesheet" href="<?=BASEURL?>static/normalize.css">
|
||||||
|
<link type="text/css" rel="stylesheet" href="<?=BASEURL?>static/style.css">
|
||||||
|
<script type="text/javascript" src="<?=BASEURL?>static/site.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<div id="content">
|
||||||
|
<?=file_get_contents(BASEDIR.'/header.htm')?>
|
||||||
|
<?=$content?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function listprojects() {
|
||||||
|
// List projects
|
||||||
|
|
||||||
|
$ls = scandir(BASEDIR.'data');
|
||||||
|
rsort($ls);
|
||||||
|
$projects = array();
|
||||||
|
foreach($ls as $dirname) {
|
||||||
|
if ($dirname[0] == '.') continue;
|
||||||
|
if (! is_dir(BASEDIR.'data/'.$dirname)) continue;
|
||||||
|
$matches = array();
|
||||||
|
|
||||||
|
if (preg_match('~(?:\d+-)?(.+)~', $dirname, $matches)) {
|
||||||
|
$projects[$dirname] = $matches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildprojects($id, $projects) {
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach($projects as $dirname => $projectname) {
|
||||||
|
|
||||||
|
echo sprintf("@%1d [%3d/%3d] ".$projectname."...\n", $id, ++$count, count($projects));
|
||||||
|
|
||||||
|
$pr = new CProject($dirname, $projectname);
|
||||||
|
$pr->write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildcommon() {
|
||||||
|
|
||||||
|
echo "@0 [ 0/ ?] Common files...\n";
|
||||||
|
|
||||||
|
$projects = listprojects();
|
||||||
|
|
||||||
|
// Build all projects
|
||||||
|
|
||||||
|
$plist = array();
|
||||||
|
|
||||||
|
$handles = array();
|
||||||
|
$handle_lookup = array();
|
||||||
|
|
||||||
|
$alphasort = [];
|
||||||
|
|
||||||
|
foreach($projects as $dirname => $projectname) {
|
||||||
|
|
||||||
|
$pr = new CProject($dirname, $projectname);
|
||||||
|
$pr->genHomeImage(); // thumbnail
|
||||||
|
|
||||||
|
$plist[] = $pr;
|
||||||
|
|
||||||
|
if (is_null($pr->homeimage)) {
|
||||||
|
$handle_lookup[$projectname] = null;
|
||||||
|
} else {
|
||||||
|
$handle_lookup[$projectname] = count($handles);
|
||||||
|
$handles[] = $pr->homeimage;
|
||||||
|
}
|
||||||
|
|
||||||
|
$alphasort[] = [$pr->projname, count($plist)-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($alphasort, function($a, $b) {
|
||||||
|
return strcasecmp($a[0], $b[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$alphaidx = [];
|
||||||
|
|
||||||
|
foreach($alphasort as $a) {
|
||||||
|
$alphaidx[ $a[1] ] = count($alphaidx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build homepage spritesheet
|
||||||
|
|
||||||
|
if (count($handles)) {
|
||||||
|
mkspritesheet($handles, BASEDIR.'wwwroot/logos.jpg', INDEX_THUMB_W, INDEX_THUMB_H);
|
||||||
|
array_map('imagedestroy', $handles); // free
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache-busting stylesheet
|
||||||
|
|
||||||
|
$style = '.homeimage-sprite { background-image: url("logos.jpg?'.md5_file(BASEDIR.'wwwroot/logos.jpg').'"); }';
|
||||||
|
|
||||||
|
// Build index page
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if (file_exists(BASEDIR.'homepage_blurb.htm')) { ?>
|
||||||
|
<!-- homepage blurb {{ -->
|
||||||
|
<?=file_get_contents(BASEDIR.'homepage_blurb.htm')?>
|
||||||
|
<!-- }} -->
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
<?php echo $style; ?>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<table id="projtable-main" class="projtable">
|
||||||
|
<?php foreach ($plist as $i => $pr) { ?>
|
||||||
|
<tr class="<?=$pr->getClassAttr()?>"
|
||||||
|
data-sort-mt="-<?=$pr->lastupdate?>"
|
||||||
|
data-sort-ct="<?=$i?>"
|
||||||
|
data-sort-al="<?=$alphaidx[$i]?>"
|
||||||
|
data-sort-nr="-<?=$pr->numreleases?>"
|
||||||
|
data-sort-nf="-<?=$pr->numDownloads()?>"
|
||||||
|
data-sort-ls="-<?=$pr->lifespan?>"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<a href="<?=hesc(BASEURL.urlencode($pr->projname))?>/"><?=(is_null($handle_lookup[$pr->projname]) ? '<div class="no-image"></div>' : '<div class="homeimage homeimage-sprite" style="background-position:0 -'.($handle_lookup[$pr->projname]*INDEX_THUMB_H).'px"></div>')?></a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong><?=hesc(str_replace('_', ' ', $pr->projname))?></strong><?php if (SHOW_BLURBS) { ?>,
|
||||||
|
<?=hesc($pr->shortdesc)?>
|
||||||
|
<?php } ?>
|
||||||
|
<a href="<?=hesc(BASEURL.urlencode($pr->projname))?>/" class="article-read-more">more...</a>
|
||||||
|
<?php if (strlen($pr->subtag) || count($pr->tags)) { ?>
|
||||||
|
<br>
|
||||||
|
<small>
|
||||||
|
<?=hesc($pr->subtag)?>
|
||||||
|
<?php if (strlen($pr->subtag) && count($pr->tags)) { ?>
|
||||||
|
::
|
||||||
|
<?php } ?>
|
||||||
|
<?php foreach($pr->tags as $tag) { ?>
|
||||||
|
<a class="tag tag-link" data-tag="<?=hesc($tag)?>"><?=hesc($tag)?></a>
|
||||||
|
<?php } ?>
|
||||||
|
</small>
|
||||||
|
<?php } ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php } ?>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$extra_head = '<link rel="canonical" href="'.hesc(BASEURL).'">';
|
||||||
|
|
||||||
|
$index = template(SITE_TITLE, ob_get_clean(), $extra_head);
|
||||||
|
file_put_contents(BASEDIR.'wwwroot/index.html', $index);
|
||||||
|
|
||||||
|
// Done
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirecthtml($target) {
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<meta http-equiv="refresh" content="0; url=<?=hesc($target)?>">
|
||||||
|
<a href="<?=hesc($target)?>">Moved »</a>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildredirects($redirects) {
|
||||||
|
foreach($redirects as $oldname => $newname) {
|
||||||
|
|
||||||
|
$page = redirecthtml(BASEURL.$newname.'/');
|
||||||
|
|
||||||
|
// old format
|
||||||
|
file_put_contents(BASEDIR.'wwwroot/'.$oldname.'.html', $page);
|
||||||
|
|
||||||
|
// new format
|
||||||
|
mkdir(BASEDIR.'wwwroot/'.$oldname);
|
||||||
|
file_put_contents(BASEDIR.'wwwroot/'.$oldname.'/index.html', $page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildgosubpackages($packages) {
|
||||||
|
foreach($packages as $path => $goGetStr) {
|
||||||
|
|
||||||
|
$page = (
|
||||||
|
'<meta name="go-import" content="'.hesc($goGetStr).'">'.
|
||||||
|
"\n".
|
||||||
|
redirecthtml(BASEURL)
|
||||||
|
);
|
||||||
|
|
||||||
|
// new directory format only
|
||||||
|
mkdir_all(BASEDIR.'wwwroot/'.$path);
|
||||||
|
file_put_contents(BASEDIR.'wwwroot/'.$path.'/index.html', $page);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
191
lib/util.php
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function mkshield($left_str, $right_str, $color_str, $params=[]) {
|
||||||
|
$filename = sprintf(
|
||||||
|
"%s-%s-%s.svg",
|
||||||
|
rawurlencode(str_replace('-', '--', $left_str)),
|
||||||
|
rawurlencode(str_replace('-', '--', $right_str)),
|
||||||
|
rawurlencode($color_str)
|
||||||
|
);
|
||||||
|
if (count($params) > 0) {
|
||||||
|
$filename .= '?' . http_build_query($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache_path = SHIELDS_CACHE_DIR.urlencode($filename);
|
||||||
|
|
||||||
|
if (file_exists($cache_path)) {
|
||||||
|
return file_get_contents($cache_path);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$retn = file_get_contents('https://img.shields.io/badge/'.$filename);
|
||||||
|
|
||||||
|
// We need unique IDs
|
||||||
|
$prefix = substr(sha1($filename), 8).'-';
|
||||||
|
$retn = str_replace('id="', 'id="'.$prefix, $retn);
|
||||||
|
$retn = str_replace('url(#', 'url(#'.$prefix, $retn);
|
||||||
|
|
||||||
|
file_put_contents($cache_path, $retn);
|
||||||
|
return $retn;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a thumbnail of an image. It overscales, centers, and crops to fit the
|
||||||
|
* target dimensions.
|
||||||
|
*
|
||||||
|
* @param string $src_file
|
||||||
|
* @param string $dest_file Null to return an image handle
|
||||||
|
* @param int $width
|
||||||
|
* @param int $height
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
function mkthumbnail($src_file, $dest_file, $width, $height) {
|
||||||
|
list($src_width, $src_height) = getimagesize($src_file);
|
||||||
|
|
||||||
|
$im = imagecreatefromstring(file_get_contents($src_file));
|
||||||
|
|
||||||
|
$dest = imagecreatetruecolor($width, $height);
|
||||||
|
imagefilledrectangle($dest, 0, 0, $width, $height, imagecolorallocate($dest, 0xFF, 0xFF, 0xFF));
|
||||||
|
|
||||||
|
$scale = max( $width/$src_width, $height/$src_height ); // overscale + crop
|
||||||
|
|
||||||
|
$box_w = $width/$scale;
|
||||||
|
$box_h = $height/$scale;
|
||||||
|
|
||||||
|
$box_xoff = floor(($src_width - $box_w)/2);
|
||||||
|
$box_yoff = floor(($src_height - $box_h)/2);
|
||||||
|
|
||||||
|
imagecopyresampled(
|
||||||
|
$dest, $im,
|
||||||
|
0, 0,
|
||||||
|
$box_xoff, $box_yoff,
|
||||||
|
$width, $height, $box_w, $box_h
|
||||||
|
);
|
||||||
|
|
||||||
|
imagedestroy($im);
|
||||||
|
|
||||||
|
if (is_null($dest_file)) {
|
||||||
|
return $dest;
|
||||||
|
} else {
|
||||||
|
return imagejpeg($dest, $dest_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkspritesheet(array $handles, $dest_file, $width, $height) {
|
||||||
|
$im = imagecreatetruecolor($width, $height * count($handles));
|
||||||
|
|
||||||
|
for($i = 0, $e = count($handles); $i != $e; ++$i) {
|
||||||
|
imagecopy($im, $handles[$i], 0, $i * $height, 0, 0, $width, $height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_null($dest_file)) {
|
||||||
|
return $dest_file;
|
||||||
|
} else {
|
||||||
|
return imagejpeg($im, $dest_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fbytes($size, $suffixes='B|KiB|MiB|GiB|TiB') {
|
||||||
|
$sxlist = explode('|', $suffixes);
|
||||||
|
if ($size < 1024) {
|
||||||
|
return $size.$sxlist[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($size > 1024 && count($sxlist) >= 2) {
|
||||||
|
array_shift($sxlist);
|
||||||
|
$size /= 1024;
|
||||||
|
}
|
||||||
|
return number_format($size, 2).' '.array_shift($sxlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
function str_ext($sz) {
|
||||||
|
$dpos = strrpos($sz, '.');
|
||||||
|
return substr($sz, $dpos+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_image($sz) {
|
||||||
|
return in_array(strtolower(str_ext($sz)), ['jpg', 'png', 'jpeg']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hesc($sz) {
|
||||||
|
return @htmlentities($sz, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkdir_all($path) {
|
||||||
|
$epath = escapeshellarg($path);
|
||||||
|
`mkdir -p ${epath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function text2html($sz) {
|
||||||
|
|
||||||
|
$identity = function($sz) {
|
||||||
|
return $sz;
|
||||||
|
};
|
||||||
|
|
||||||
|
$splitInside = function($begin, $end, $sz) {
|
||||||
|
$parts = explode($begin, $sz);
|
||||||
|
if (count($parts) == 1) return [$sz];
|
||||||
|
|
||||||
|
$ret = [$parts[0]];
|
||||||
|
for($i = 1, $e = count($parts); $i !== $e; ++$i) {
|
||||||
|
$inner = explode($end, $parts[$i], 2);
|
||||||
|
$ret = array_merge($ret, $inner);
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
$oddEven = function(array $parts, $odd, $even, $join='') {
|
||||||
|
$ret = [];
|
||||||
|
for($i = 0, $e = count($parts); $i != $e; ++$i) {
|
||||||
|
$ret[] = ($i % 2) ? $odd($parts[$i]) : $even($parts[$i]);
|
||||||
|
}
|
||||||
|
return implode($join, $ret);
|
||||||
|
};
|
||||||
|
|
||||||
|
$sectionFmt = function($sz) use($oddEven, $identity) {
|
||||||
|
$base = hesc($sz);
|
||||||
|
|
||||||
|
$base = preg_replace('~^=+(.+)=+~m', '<strong>\\1</strong>', $base);
|
||||||
|
$base = preg_replace('~\\[url=([^\\]]+?)\\](.+?)\\[/url\\]~m', '<a href="\\1">\\2</a>', $base);
|
||||||
|
|
||||||
|
$base = preg_replace('~\\[([^\\]]+?)\\]\\((https?://.+?)\\)~m', '<a href="\\2">\\1</a>', $base); // Support markdown-style URLs
|
||||||
|
|
||||||
|
$base = preg_replace('~([^="])(https?://[^ \\r\\n\\t]+)~i', '\\1<a href="\\2">\\2</a>', $base); // Support standalone URLs
|
||||||
|
$base = preg_replace('~\\[b\\](.+?)\\[/b\\]~m', '<strong>\\1</strong>', $base);
|
||||||
|
$base = preg_replace('~\\[i\\](.+?)\\[/i\\]~m', '<i>\\1</i>', $base);
|
||||||
|
$base = preg_replace('~\\[spoiler\\](.+?)\\[/spoiler\\]~m', '<span class="spoiler">\\1</span>', $base);
|
||||||
|
$base = preg_replace('~\n- ~ms', "\n• ", $base);
|
||||||
|
|
||||||
|
$base = preg_replace('~^```.+$~m', '`', $base); // Convert ```html to single `{}` element
|
||||||
|
|
||||||
|
// TODO support markdown tables
|
||||||
|
|
||||||
|
$btparts = explode('`', $base);
|
||||||
|
if (count($btparts) > 1 && (count($btparts) % 2)) {
|
||||||
|
for ($i = 1, $e = count($btparts); $i < $e; $i += 2) {
|
||||||
|
$class = 'code';
|
||||||
|
if (strpos($btparts[$i], "\n") !== false) {
|
||||||
|
$class .= ' code-multiline';
|
||||||
|
}
|
||||||
|
$btparts[$i] = '<span class="'.$class.'">'.ltrim($btparts[$i], "\n").'</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $oddEven($btparts, $identity, 'nl2br');
|
||||||
|
};
|
||||||
|
|
||||||
|
$htmlSections = $splitInside('<html>', '</html>', $sz);
|
||||||
|
return $oddEven($htmlSections, $identity, $sectionFmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function array_decimate($array, $total, $partno) {
|
||||||
|
$ct = 0;
|
||||||
|
$ret = [];
|
||||||
|
foreach($array as $k => $v) {
|
||||||
|
if (++$ct % $total == ($partno - 1)) {
|
||||||
|
$ret[$k] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $ret;
|
||||||
|
}
|
14
mkdist.sh
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
tar cJvf "codesite-$(date +%s).tar.xz" \
|
||||||
|
--exclude='sites/codesite.example.com/wwwroot' \
|
||||||
|
rebuild.sh \
|
||||||
|
sites/codesite.example.com \
|
||||||
|
static \
|
||||||
|
lib \
|
||||||
|
--owner=0 --group=0
|
||||||
|
|
||||||
|
read -p "Press any key to continue..."
|
||||||
|
|
@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
C:\path\to\php_54\php.exe rebuild.php
|
|
||||||
pause
|
|
453
rebuild.php
@ -1,453 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// Code-hosting website
|
|
||||||
// ````````````````````
|
|
||||||
|
|
||||||
// CONFIGURATION
|
|
||||||
// `````````````
|
|
||||||
|
|
||||||
define('BASEDIR', __DIR__.'\\');
|
|
||||||
define('SITE_TITLE', 'codesite');
|
|
||||||
define('PAGE_THUMB_W', 60);
|
|
||||||
define('PAGE_THUMB_H', 60);
|
|
||||||
define('INDEX_THUMB_W', 90);
|
|
||||||
define('INDEX_THUMB_H', 32); // recommend a multiple of the jpg iDCT block size
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a thumbnail of an image. It overscales, centers, and crops to fit the
|
|
||||||
* target dimensions.
|
|
||||||
*
|
|
||||||
* @param string $src_file
|
|
||||||
* @param string $dest_file Null to return an image handle
|
|
||||||
* @param int $width
|
|
||||||
* @param int $height
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
function mkthumbnail($src_file, $dest_file, $width, $height) {
|
|
||||||
list($src_width, $src_height) = getimagesize($src_file);
|
|
||||||
|
|
||||||
$im = imagecreatefromstring(file_get_contents($src_file));
|
|
||||||
|
|
||||||
$dest = imagecreatetruecolor($width, $height);
|
|
||||||
imagefilledrectangle($dest, 0, 0, $width, $height, imagecolorallocate($dest, 0xFF, 0xFF, 0xFF));
|
|
||||||
|
|
||||||
$scale = max( $width/$src_width, $height/$src_height ); // overscale + crop
|
|
||||||
|
|
||||||
$box_w = $width/$scale;
|
|
||||||
$box_h = $height/$scale;
|
|
||||||
|
|
||||||
$box_xoff = floor(($src_width - $box_w)/2);
|
|
||||||
$box_yoff = floor(($src_height - $box_h)/2);
|
|
||||||
|
|
||||||
imagecopyresampled(
|
|
||||||
$dest, $im,
|
|
||||||
0, 0,
|
|
||||||
$box_xoff, $box_yoff,
|
|
||||||
$width, $height, $box_w, $box_h
|
|
||||||
);
|
|
||||||
|
|
||||||
imagedestroy($im);
|
|
||||||
|
|
||||||
if (is_null($dest_file)) {
|
|
||||||
return $dest;
|
|
||||||
} else {
|
|
||||||
return imagejpeg($dest, $dest_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mkspritesheet(array $handles, $dest_file, $width, $height) {
|
|
||||||
$im = imagecreatetruecolor($width, $height * count($handles));
|
|
||||||
|
|
||||||
for($i = 0, $e = count($handles); $i != $e; ++$i) {
|
|
||||||
imagecopy($im, $handles[$i], 0, $i * $height, 0, 0, $width, $height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_null($dest_file)) {
|
|
||||||
return $dest_file;
|
|
||||||
} else {
|
|
||||||
return imagejpeg($im, $dest_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fbytes($size, $suffixes='B|KiB|MiB|GiB|TiB') {
|
|
||||||
$sxlist = explode('|', $suffixes);
|
|
||||||
if ($size < 1024) {
|
|
||||||
return $size.$sxlist[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
while ($size > 1024 && count($sxlist) >= 2) {
|
|
||||||
array_shift($sxlist);
|
|
||||||
$size /= 1024;
|
|
||||||
}
|
|
||||||
return number_format($size, 2).array_shift($sxlist);
|
|
||||||
}
|
|
||||||
|
|
||||||
function str_ext($sz) {
|
|
||||||
$dpos = strrpos($sz, '.');
|
|
||||||
return substr($sz, $dpos+1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function is_image($sz) {
|
|
||||||
return in_array(strtolower(str_ext($sz)), ['jpg', 'png', 'jpeg']);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hesc($sz) {
|
|
||||||
return @htmlentities($sz, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function text2html($sz) {
|
|
||||||
|
|
||||||
$sectionFmt = function($sz) {
|
|
||||||
$base = hesc($sz);
|
|
||||||
|
|
||||||
$base = preg_replace('~=+(.+)=+~', '<strong>\\1</strong>', $base);
|
|
||||||
$base = preg_replace('~(https?://[^ \\r\\n\\t]+)~i', '<a href="\\1">\\1</a>', $base);
|
|
||||||
|
|
||||||
$btparts = explode('`', $base);
|
|
||||||
if (count($btparts) > 1 && (count($btparts) % 2)) {
|
|
||||||
for ($i = 1, $e = count($btparts); $i < $e; $i += 2) {
|
|
||||||
$btparts[$i] = '<span class="code">'.$btparts[$i].'</span>';
|
|
||||||
}
|
|
||||||
$base = implode('', $btparts);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nl2br($base);
|
|
||||||
};
|
|
||||||
|
|
||||||
$identity = function($sz) {
|
|
||||||
return $sz;
|
|
||||||
};
|
|
||||||
|
|
||||||
$splitInside = function($begin, $end, $sz) {
|
|
||||||
$parts = explode($begin, $sz);
|
|
||||||
if (count($parts) == 1) return [$sz];
|
|
||||||
|
|
||||||
$ret = [$parts[0]];
|
|
||||||
for($i = 1, $e = count($parts); $i !== $e; ++$i) {
|
|
||||||
$inner = explode($end, $parts[$i], 2);
|
|
||||||
$ret = array_merge($ret, $inner);
|
|
||||||
}
|
|
||||||
return $ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
$oddEven = function(array $parts, $odd, $even, $join='') {
|
|
||||||
$ret = [];
|
|
||||||
for($i = 0, $e = count($parts); $i != $e; ++$i) {
|
|
||||||
$ret[] = ($i % 2) ? $odd($parts[$i]) : $even($parts[$i]);
|
|
||||||
}
|
|
||||||
return implode($join, $ret);
|
|
||||||
};
|
|
||||||
|
|
||||||
$htmlSections = $splitInside('<html>', '</html>', $sz);
|
|
||||||
return $oddEven($htmlSections, $identity, $sectionFmt);
|
|
||||||
}
|
|
||||||
|
|
||||||
function array_decimate($array, $total, $partno) {
|
|
||||||
$ct = 0;
|
|
||||||
$ret = [];
|
|
||||||
foreach($array as $k => $v) {
|
|
||||||
if (++$ct % $total == ($partno - 1)) {
|
|
||||||
$ret[$k] = $v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class CProject {
|
|
||||||
|
|
||||||
private $dir;
|
|
||||||
public $projname;
|
|
||||||
public $shortdesc = '(no description)';
|
|
||||||
public $subtag = '';
|
|
||||||
private $longdesc = '';
|
|
||||||
private $images = array();
|
|
||||||
private $downloads = array();
|
|
||||||
public $tags = array();
|
|
||||||
|
|
||||||
public $homeimage = null;
|
|
||||||
|
|
||||||
public function __construct($dirname, $projname) {
|
|
||||||
$this->dir = BASEDIR.'data/'.$dirname.'/';
|
|
||||||
$this->projname = $projname;
|
|
||||||
|
|
||||||
// Identify resources in folder
|
|
||||||
|
|
||||||
$ls = scandir($this->dir);
|
|
||||||
foreach($ls as $file) {
|
|
||||||
if ($file[0] == '.') continue;
|
|
||||||
|
|
||||||
if ($file == 'README.txt') {
|
|
||||||
$this->longdesc = file_get_contents($this->dir.'README.txt');
|
|
||||||
$matches = array();
|
|
||||||
if (preg_match('~Written in ([^\\r\\n]+)~', $this->longdesc, $matches)) {
|
|
||||||
$this->subtag = rtrim($matches[1], ' .');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('~Tags: ([^\\r\\n]+)~', $this->longdesc, $matches)) {
|
|
||||||
$this->tags = array_map('trim', explode(',', $matches[1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = explode("\n", $this->longdesc);
|
|
||||||
$this->shortdesc = array_shift($parts);
|
|
||||||
$this->shortdesc[0] = strtolower($this->shortdesc[0]); // cosmetic lowercase
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_image($file)) {
|
|
||||||
$this->images[] = $file;
|
|
||||||
} else {
|
|
||||||
$this->downloads[] = $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
natcasesort($this->downloads);
|
|
||||||
$this->downloads = array_reverse($this->downloads);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function genHomeImage() {
|
|
||||||
if (count($this->images)) {
|
|
||||||
|
|
||||||
$this->homeimage = mkthumbnail(
|
|
||||||
$this->dir.$this->images[0], //BASEDIR.'wwwroot/srv/'.$this->projname.'_0.'.str_ext($this->images[0]),
|
|
||||||
null, // raw handle
|
|
||||||
INDEX_THUMB_W, INDEX_THUMB_H
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function write() {
|
|
||||||
|
|
||||||
// Generate image thumbnails
|
|
||||||
|
|
||||||
foreach($this->images as $idx => $image) {
|
|
||||||
$outfile = BASEDIR.'wwwroot/srv/'.$this->projname.'_'.$idx;
|
|
||||||
copy($this->dir.$image, $outfile.'.'.str_ext($image));
|
|
||||||
|
|
||||||
mkthumbnail($outfile.'.'.str_ext($image), $outfile.'_thumb.jpg', PAGE_THUMB_W, PAGE_THUMB_H);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy downloads to wwwroot
|
|
||||||
|
|
||||||
foreach($this->downloads as $idx => $filename) {
|
|
||||||
copy($this->dir.$filename, BASEDIR.'wwwroot/srv/'.$filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate index page
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
$this->index();
|
|
||||||
$idxfile = template($this->projname.' | '.SITE_TITLE, ob_get_clean());
|
|
||||||
file_put_contents(BASEDIR.'wwwroot/'.$this->projname.'.html', $idxfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClassAttr() {
|
|
||||||
if (count($this->tags)) {
|
|
||||||
return 'taggedWith-'.implode(' taggedWith-', $this->tags);
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function index() {
|
|
||||||
?>
|
|
||||||
<h2><?=hesc($this->projname)?></h2>
|
|
||||||
|
|
||||||
<div class="projinfo">
|
|
||||||
|
|
||||||
<div class="projbody projbody_<?=(count($this->images) ? 'half' : 'full')?>w">
|
|
||||||
|
|
||||||
<strong>ABOUT</strong>
|
|
||||||
|
|
||||||
<p><?=text2html($this->longdesc)?></p>
|
|
||||||
|
|
||||||
<?=file_get_contents(__DIR__.'/footer.htm')?>
|
|
||||||
|
|
||||||
<?php if (count($this->downloads)) { ?>
|
|
||||||
|
|
||||||
<strong>DOWNLOAD</strong>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<?php foreach($this->downloads as $filename) { ?>
|
|
||||||
<li>
|
|
||||||
<a href="srv/<?=hesc(rawurlencode($filename))?>"><?=hesc($filename)?></a>
|
|
||||||
<small>
|
|
||||||
<?=hesc(fbytes(filesize(BASEDIR.'wwwroot/srv/'.$filename)))?>
|
|
||||||
</small>
|
|
||||||
</li>
|
|
||||||
<?php } ?>
|
|
||||||
</ul>
|
|
||||||
<?php } ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (count($this->images)) { ?>
|
|
||||||
<div class="projimg">
|
|
||||||
<?php foreach($this->images as $idx => $origname) { ?>
|
|
||||||
<a href="srv/<?=hesc(urlencode($this->projname))?>_<?=$idx?>.<?=str_ext($origname)?>"><img src="srv/<?=hesc(urlencode($this->projname))?>_<?=$idx?>_thumb.jpg" class="thumbimage"></a>
|
|
||||||
<?php } ?>
|
|
||||||
</div>
|
|
||||||
<?php } ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function template($title, $content) {
|
|
||||||
ob_start();
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" >
|
|
||||||
<meta name="viewport" content="width=768px" >
|
|
||||||
<link type="text/css" rel="stylesheet" href="normalize.css">
|
|
||||||
<link type="text/css" rel="stylesheet" href="style.css">
|
|
||||||
<script type="text/javascript" src="site.js"></script>
|
|
||||||
<title><?=hesc($title)?></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="container">
|
|
||||||
<div id="content">
|
|
||||||
<?=file_get_contents(__DIR__.'/header.htm')?>
|
|
||||||
<?=$content?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
<?php
|
|
||||||
return ob_get_clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
function listprojects() {
|
|
||||||
// List projects
|
|
||||||
|
|
||||||
$ls = scandir(BASEDIR.'data');
|
|
||||||
rsort($ls);
|
|
||||||
$projects = array();
|
|
||||||
foreach($ls as $dirname) {
|
|
||||||
if ($dirname[0] == '.') continue;
|
|
||||||
$matches = array();
|
|
||||||
|
|
||||||
if (preg_match('~(?:\d+-)?(.+)~', $dirname, $matches)) {
|
|
||||||
$projects[$dirname] = $matches[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $projects;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildprojects($id, $projects) {
|
|
||||||
$count = 0;
|
|
||||||
|
|
||||||
foreach($projects as $dirname => $projectname) {
|
|
||||||
|
|
||||||
echo sprintf("@%1d [%3d/%3d] ".$projectname."...\n", $id, ++$count, count($projects));
|
|
||||||
|
|
||||||
$pr = new CProject($dirname, $projectname);
|
|
||||||
$pr->write();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildcommon() {
|
|
||||||
|
|
||||||
echo "@0 [ 0/ ?] Common files...\n";
|
|
||||||
|
|
||||||
$projects = listprojects();
|
|
||||||
|
|
||||||
// Build all projects
|
|
||||||
|
|
||||||
$plist = array();
|
|
||||||
|
|
||||||
$handles = array();
|
|
||||||
$handle_lookup = array();
|
|
||||||
|
|
||||||
foreach($projects as $dirname => $projectname) {
|
|
||||||
|
|
||||||
$pr = new CProject($dirname, $projectname);
|
|
||||||
$pr->genHomeImage(); // thumbnail
|
|
||||||
|
|
||||||
$plist[] = $pr;
|
|
||||||
|
|
||||||
if (is_null($pr->homeimage)) {
|
|
||||||
$handle_lookup[$projectname] = null;
|
|
||||||
} else {
|
|
||||||
$handle_lookup[$projectname] = count($handles);
|
|
||||||
$handles[] = $pr->homeimage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build homepage spritesheet
|
|
||||||
|
|
||||||
mkspritesheet($handles, BASEDIR.'wwwroot/logos.jpg', INDEX_THUMB_W, INDEX_THUMB_H);
|
|
||||||
array_map('imagedestroy', $handles); // free
|
|
||||||
|
|
||||||
// Build index page
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<?php if (file_exists(BASEDIR.'homepage_blurb.htm')) { ?>
|
|
||||||
<!-- homepage blurb {{ -->
|
|
||||||
<?=file_get_contents(BASEDIR.'homepage_blurb.htm')?>
|
|
||||||
<!-- }} -->
|
|
||||||
<?php } ?>
|
|
||||||
|
|
||||||
<table class="projtable">
|
|
||||||
<?php foreach ($plist as $pr) { ?>
|
|
||||||
<tr class="<?=$pr->getClassAttr()?>">
|
|
||||||
<td>
|
|
||||||
<a href="<?=hesc(urlencode($pr->projname))?>.html"><?=(is_null($handle_lookup[$pr->projname]) ? '<div class="no-image"></div>' : '<div class="homeimage homeimage-sprite" style="background-position:0 -'.($handle_lookup[$pr->projname]*INDEX_THUMB_H).'px"></div>')?></a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<strong><?=hesc($pr->projname)?></strong>,
|
|
||||||
<?=hesc($pr->shortdesc)?>
|
|
||||||
<a href="<?=hesc(urlencode($pr->projname))?>.html">more...</a>
|
|
||||||
<?php if (strlen($pr->subtag) || count($pr->tags)) { ?>
|
|
||||||
<br>
|
|
||||||
<small>
|
|
||||||
<?=hesc($pr->subtag)?>
|
|
||||||
<?php if (strlen($pr->subtag) && count($pr->tags)) { ?>
|
|
||||||
::
|
|
||||||
<?php } ?>
|
|
||||||
<?php foreach($pr->tags as $tag) { ?>
|
|
||||||
<a class="tag tag-link" data-tag="<?=hesc($tag)?>"><?=hesc($tag)?></a>
|
|
||||||
<?php } ?>
|
|
||||||
</small>
|
|
||||||
<?php } ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php } ?>
|
|
||||||
</table>
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$index = template(SITE_TITLE, ob_get_clean());
|
|
||||||
file_put_contents(BASEDIR.'wwwroot/index.html', $index);
|
|
||||||
|
|
||||||
// Done
|
|
||||||
}
|
|
||||||
|
|
||||||
function main($args) {
|
|
||||||
$total = $args[0];
|
|
||||||
$pos = $args[1];
|
|
||||||
|
|
||||||
if ($pos == 0) {
|
|
||||||
buildcommon();
|
|
||||||
} else {
|
|
||||||
buildprojects($pos, array_decimate(listprojects(), $total, $pos));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry point
|
|
||||||
//
|
|
||||||
|
|
||||||
ini_set('display_errors', 'On');
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
main(array_slice($_SERVER['argv'], 1));
|
|
66
rebuild.sh
Executable file
@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
APP_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
numcpus() {
|
||||||
|
cat /proc/cpuinfo | grep '^processor' | wc -l
|
||||||
|
}
|
||||||
|
|
||||||
|
buildsite() {
|
||||||
|
local site="$1"
|
||||||
|
|
||||||
|
echo "Site: ${site}"
|
||||||
|
(
|
||||||
|
cd "$site"
|
||||||
|
|
||||||
|
echo "Cleaning wwwroot directory..."
|
||||||
|
|
||||||
|
if [[ -d wwwroot ]] ; then
|
||||||
|
rm -r wwwroot
|
||||||
|
fi
|
||||||
|
mkdir -p wwwroot/{img,srv,static}
|
||||||
|
|
||||||
|
echo "Copying static resources..."
|
||||||
|
|
||||||
|
if [[ ! -d static ]] ; then
|
||||||
|
mkdir static
|
||||||
|
fi
|
||||||
|
cp "${APP_DIR}/static/"* wwwroot/static || true
|
||||||
|
cp static/* wwwroot/static || true
|
||||||
|
|
||||||
|
for htm in footer header homepage_blurb ; do
|
||||||
|
if [[ ! -f "${htm}.htm" ]] ; then
|
||||||
|
touch "${htm}.htm"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Building pages..."
|
||||||
|
|
||||||
|
local threadcount=$(numcpus)
|
||||||
|
for i in $(seq 0 "$threadcount") ; do
|
||||||
|
"${APP_DIR}/build-worker" "$threadcount" "$i" &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
|
||||||
|
echo "Site: ${site} finished."
|
||||||
|
echo ""
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "USAGE: ./rebuild.sh path-to-siteroot"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ $# -ne 1 ]] ; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
buildsite "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
0
shields_cache/.create_dir
Normal file
13
sites/codesite.example.com/config.ini
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[codesite]
|
||||||
|
title=codesite.example.com
|
||||||
|
page_thumb_w=60
|
||||||
|
page_thumb_h=60
|
||||||
|
index_thumb_w=90
|
||||||
|
index_thumb_h=32
|
||||||
|
|
||||||
|
; n.b. Recommend a multiple of the JPEG iDCT block size for index_thumb_h
|
||||||
|
|
||||||
|
[redirect]
|
||||||
|
; old project name = new project name
|
||||||
|
old-project-name=example-project
|
||||||
|
|
14
sites/codesite.example.com/homepage_blurb.htm
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
<p>
|
||||||
|
Homepage blurb goes here.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<select id="sortorder" style="float:right;">
|
||||||
|
<option value="a">Youngest project first</option>
|
||||||
|
<option value="b">Recent updates first</option>
|
||||||
|
<option value="c">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<strong>PROJECTS</strong>
|
||||||
|
</p>
|
Before Width: | Height: | Size: 436 B After Width: | Height: | Size: 436 B |
Before Width: | Height: | Size: 233 B After Width: | Height: | Size: 233 B |
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 264 B |
Before Width: | Height: | Size: 2.3 KiB |
541
static/normalize.css
vendored
@ -1,406 +1,419 @@
|
|||||||
/*! normalize.css v2.1.3 | MIT License | git.io/normalize */
|
/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
/* ==========================================================================
|
/**
|
||||||
HTML5 display definitions
|
* 1. Change the default font family in all browsers (opinionated).
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in IE and iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif; /* 1 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers (opinionated).
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display definitions
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Correct `block` display not defined in IE 8/9.
|
* Add the correct display in IE 9-.
|
||||||
|
* 1. Add the correct display in Edge, IE, and Firefox.
|
||||||
|
* 2. Add the correct display in IE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
article,
|
article,
|
||||||
aside,
|
aside,
|
||||||
details,
|
details, /* 1 */
|
||||||
figcaption,
|
figcaption,
|
||||||
figure,
|
figure,
|
||||||
footer,
|
footer,
|
||||||
header,
|
header,
|
||||||
hgroup,
|
main, /* 2 */
|
||||||
main,
|
menu,
|
||||||
nav,
|
nav,
|
||||||
section,
|
section,
|
||||||
summary {
|
summary { /* 1 */
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Correct `inline-block` display not defined in IE 8/9.
|
* Add the correct display in IE 9-.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
audio,
|
audio,
|
||||||
canvas,
|
canvas,
|
||||||
|
progress,
|
||||||
video {
|
video {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent modern browsers from displaying `audio` without controls.
|
* Add the correct display in iOS 4-7.
|
||||||
* Remove excess height in iOS 5 devices.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
audio:not([controls]) {
|
audio:not([controls]) {
|
||||||
display: none;
|
display: none;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address `[hidden]` styling not present in IE 8/9.
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
* Hide the `template` element in IE, Safari, and Firefox < 22.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
[hidden],
|
progress {
|
||||||
template {
|
vertical-align: baseline;
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/**
|
||||||
Base
|
* Add the correct display in IE 10-.
|
||||||
|
* 1. Add the correct display in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template, /* 1 */
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Set default font family to sans-serif.
|
* 1. Remove the gray background on active links in IE 10.
|
||||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
|
||||||
* user zoom.
|
|
||||||
*/
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-family: sans-serif; /* 1 */
|
|
||||||
-ms-text-size-adjust: 100%; /* 2 */
|
|
||||||
-webkit-text-size-adjust: 100%; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove default margin.
|
|
||||||
*/
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
Links
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the gray background color from active links in IE 10.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
a {
|
a {
|
||||||
background: transparent;
|
background-color: transparent; /* 1 */
|
||||||
|
-webkit-text-decoration-skip: objects; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address `outline` inconsistency between Chrome and other browsers.
|
* Remove the outline on focused links when they are also active or hovered
|
||||||
*/
|
* in all browsers (opinionated).
|
||||||
|
|
||||||
a:focus {
|
|
||||||
outline: thin dotted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Improve readability when focused and also mouse hovered in all browsers.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
a:active,
|
a:active,
|
||||||
a:hover {
|
a:hover {
|
||||||
outline: 0;
|
outline-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* Text-level semantics
|
||||||
Typography
|
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address variable `h1` font-size and margin within `section` and `article`
|
* 1. Remove the bottom border in Firefox 39-.
|
||||||
* contexts in Firefox 4+, Safari 5, and Chrome.
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
*/
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
margin: 0.67em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address styling not present in IE 8/9, Safari 5, and Chrome.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
abbr[title] {
|
abbr[title] {
|
||||||
border-bottom: 1px dotted;
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
text-decoration: underline dotted; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
|
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
b,
|
b,
|
||||||
strong {
|
strong {
|
||||||
font-weight: bold;
|
font-weight: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address styling not present in Safari 5 and Chrome.
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font style in Android 4.3-.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
dfn {
|
dfn {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address differences between Firefox and other browsers.
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
hr {
|
h1 {
|
||||||
-moz-box-sizing: content-box;
|
font-size: 2em;
|
||||||
box-sizing: content-box;
|
margin: 0.67em 0;
|
||||||
height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address styling not present in IE 8/9.
|
* Add the correct background and color in IE 9-.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
mark {
|
mark {
|
||||||
background: #ff0;
|
background-color: #ff0;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Correct font family set oddly in Safari 5 and Chrome.
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the overflow in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
svg:not(:root) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
code,
|
code,
|
||||||
kbd,
|
kbd,
|
||||||
pre,
|
pre,
|
||||||
samp {
|
samp {
|
||||||
font-family: monospace, serif;
|
font-family: monospace, monospace; /* 1 */
|
||||||
font-size: 1em;
|
font-size: 1em; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Improve readability of pre-formatted text in all browsers.
|
* Add the correct margin in IE 8.
|
||||||
*/
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set consistent quote types.
|
|
||||||
*/
|
|
||||||
|
|
||||||
q {
|
|
||||||
quotes: "\201C" "\201D" "\2018" "\2019";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address inconsistent and variable font size in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
small {
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
sub,
|
|
||||||
sup {
|
|
||||||
font-size: 75%;
|
|
||||||
line-height: 0;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
sup {
|
|
||||||
top: -0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub {
|
|
||||||
bottom: -0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
Embedded content
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove border when inside `a` element in IE 8/9.
|
|
||||||
*/
|
|
||||||
|
|
||||||
img {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Correct overflow displayed oddly in IE 9.
|
|
||||||
*/
|
|
||||||
|
|
||||||
svg:not(:root) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
Figures
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Address margin not present in IE 8/9 and Safari 5.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
margin: 0;
|
margin: 1em 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/**
|
||||||
Forms
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define consistent border, margin, and padding.
|
* 1. Change font properties to `inherit` in all browsers (opinionated).
|
||||||
*/
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
|
||||||
fieldset {
|
|
||||||
border: 1px solid #c0c0c0;
|
|
||||||
margin: 0 2px;
|
|
||||||
padding: 0.35em 0.625em 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct `color` not being inherited in IE 8/9.
|
|
||||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
|
||||||
*/
|
|
||||||
|
|
||||||
legend {
|
|
||||||
border: 0; /* 1 */
|
|
||||||
padding: 0; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct font family not being inherited in all browsers.
|
|
||||||
* 2. Correct font size not being inherited in all browsers.
|
|
||||||
* 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
font-family: inherit; /* 1 */
|
font: inherit; /* 1 */
|
||||||
font-size: 100%; /* 2 */
|
margin: 0; /* 2 */
|
||||||
margin: 0; /* 3 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
* Restore the font weight unset by the previous rule.
|
||||||
* the UA stylesheet.
|
*/
|
||||||
|
|
||||||
|
optgroup {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input {
|
input { /* 1 */
|
||||||
line-height: normal;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
* All other form control elements do not inherit `text-transform` values.
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+.
|
|
||||||
* Correct `select` style inheritance in Firefox 4+ and Opera.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
select {
|
select { /* 1 */
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
|
||||||
* and `video` controls.
|
* controls in Android 4.
|
||||||
* 2. Correct inability to style clickable `input` types in iOS.
|
* 2. Correct the inability to style clickable types in iOS and Safari.
|
||||||
* 3. Improve usability and consistency of cursor style between image-type
|
|
||||||
* `input` and others.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
html input[type="button"], /* 1 */
|
html [type="button"], /* 1 */
|
||||||
input[type="reset"],
|
[type="reset"],
|
||||||
input[type="submit"] {
|
[type="submit"] {
|
||||||
-webkit-appearance: button; /* 2 */
|
-webkit-appearance: button; /* 2 */
|
||||||
cursor: pointer; /* 3 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-set default cursor for disabled elements.
|
* Remove the inner border and padding in Firefox.
|
||||||
*/
|
|
||||||
|
|
||||||
button[disabled],
|
|
||||||
html input[disabled] {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
|
||||||
* 2. Remove excess padding in IE 8/9/10.
|
|
||||||
*/
|
|
||||||
|
|
||||||
input[type="checkbox"],
|
|
||||||
input[type="radio"] {
|
|
||||||
box-sizing: border-box; /* 1 */
|
|
||||||
padding: 0; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
|
|
||||||
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
|
|
||||||
* (include `-moz` to future-proof).
|
|
||||||
*/
|
|
||||||
|
|
||||||
input[type="search"] {
|
|
||||||
-webkit-appearance: textfield; /* 1 */
|
|
||||||
-moz-box-sizing: content-box;
|
|
||||||
-webkit-box-sizing: content-box; /* 2 */
|
|
||||||
box-sizing: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove inner padding and search cancel button in Safari 5 and Chrome
|
|
||||||
* on OS X.
|
|
||||||
*/
|
|
||||||
|
|
||||||
input[type="search"]::-webkit-search-cancel-button,
|
|
||||||
input[type="search"]::-webkit-search-decoration {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove inner padding and border in Firefox 4+.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
button::-moz-focus-inner,
|
button::-moz-focus-inner,
|
||||||
input::-moz-focus-inner {
|
[type="button"]::-moz-focus-inner,
|
||||||
border: 0;
|
[type="reset"]::-moz-focus-inner,
|
||||||
padding: 0;
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Remove default vertical scrollbar in IE 8/9.
|
* Restore the focus styles unset by the previous rule.
|
||||||
* 2. Improve readability and alignment in all browsers.
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the border, margin, and padding in all browsers (opinionated).
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0.35em 0.625em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
overflow: auto; /* 1 */
|
overflow: auto;
|
||||||
vertical-align: top; /* 2 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
Tables
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove most spacing between table cells.
|
* 1. Add the correct box sizing in IE 10-.
|
||||||
|
* 2. Remove the padding in IE 10-.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
table {
|
[type="checkbox"],
|
||||||
border-collapse: collapse;
|
[type="radio"] {
|
||||||
border-spacing: 0;
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding and cancel buttons in Chrome and Safari on OS X.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-cancel-button,
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the text style of placeholders in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
color: inherit;
|
||||||
|
opacity: 0.54;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
(function() {
|
(function() {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
//
|
||||||
|
// Tag support
|
||||||
|
//
|
||||||
|
|
||||||
var show_all = function() {
|
var show_all = function() {
|
||||||
var tr = document.querySelectorAll(".projtable tr");
|
var tr = document.querySelectorAll(".projtable tr");
|
||||||
for (var i = 0, e = tr.length; i !== e; ++i) {
|
for (var i = 0, e = tr.length; i !== e; ++i) {
|
||||||
@ -33,7 +37,10 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
var get_show_tag = function(tag) {
|
var get_show_tag = function(tag) {
|
||||||
return function() { show_tag(tag); return false; };
|
return function() {
|
||||||
|
show_tag(tag);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
@ -43,4 +50,34 @@
|
|||||||
taglinks[i].addEventListener('click', get_show_tag(tag));
|
taglinks[i].addEventListener('click', get_show_tag(tag));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
//
|
||||||
|
// Sort support (theme opt-in)
|
||||||
|
//
|
||||||
|
|
||||||
|
var sort_rows = function(cb) {
|
||||||
|
var tr = document.querySelectorAll(".projtable tr");
|
||||||
|
var items = [];
|
||||||
|
for (var i = 0, e = tr.length; i !== e; ++i) {
|
||||||
|
items.push([i, cb(tr[i])]);
|
||||||
|
}
|
||||||
|
items.sort(function(a, b) {
|
||||||
|
return (a[1] - b[1]);
|
||||||
|
});
|
||||||
|
for (var i = 0, e = items.length; i !== e; ++i) {
|
||||||
|
var el = tr[items[i][0]];
|
||||||
|
var parent = el.parentElement;
|
||||||
|
parent.removeChild(el);
|
||||||
|
parent.appendChild(el);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var sort_update = function(sort_by) {
|
||||||
|
sort_rows(function(el) {
|
||||||
|
return el.getAttribute(sort_by);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.sortUpdate = sort_update;
|
||||||
|
|
||||||
|
})();
|
||||||
|
60
update_git_remote.sh
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# This script updates/rebuilds a codesite directory from a Git repository.
|
||||||
|
#
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
main() {
|
||||||
|
|
||||||
|
if [[ $# -ne 3 ]] ; then
|
||||||
|
echo "Usage: update_git_remote DATA_DIR GIT_REMOTE_URI EXTRA_CONTENT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local datadir="$1"
|
||||||
|
local gitremote="$2"
|
||||||
|
local extra="${3:-}"
|
||||||
|
|
||||||
|
#
|
||||||
|
|
||||||
|
echo "Updating ${datadir}..."
|
||||||
|
|
||||||
|
if [[ ! -d $datadir ]] ; then
|
||||||
|
mkdir $datadir
|
||||||
|
fi
|
||||||
|
|
||||||
|
local clone=$(mktemp -d)
|
||||||
|
(
|
||||||
|
cd "${clone}"
|
||||||
|
git clone "${gitremote}" .
|
||||||
|
)
|
||||||
|
cp "${clone}/README.md" "${datadir}/README.txt"
|
||||||
|
|
||||||
|
rm -fr "${clone}"
|
||||||
|
|
||||||
|
# Ensure LF endings
|
||||||
|
sed -i 's/\x0D$//' "${datadir}/README.txt"
|
||||||
|
|
||||||
|
# Remove all shields (codesite inserts its own)
|
||||||
|
sed -i /shields.io/d "${datadir}/README.txt"
|
||||||
|
|
||||||
|
# Remove top-level header (codesite inserts its own)
|
||||||
|
sed -i -re '/^#([^#])/d' "${datadir}/README.txt"
|
||||||
|
|
||||||
|
# Remove leading blank lines (since we removed the top-level header)
|
||||||
|
sed -i '/./,$!d' "${datadir}/README.txt"
|
||||||
|
|
||||||
|
# Convert headings from github-flavored markdown to codesite style (\U for Uppercase)
|
||||||
|
sed -i -re 's/^## (.+)/=\U\1=/' "${datadir}/README.txt"
|
||||||
|
|
||||||
|
# Add extra metadata so that codesite generator can link the repository
|
||||||
|
echo "" >> "${datadir}/README.txt"
|
||||||
|
echo "[git]${gitremote}[/git]" >> "${datadir}/README.txt"
|
||||||
|
|
||||||
|
# Add any extra per-repository metadata, on a line after the description
|
||||||
|
if [[ -n "${extra}" ]] ; then
|
||||||
|
sed -i 3i"${extra}" "${datadir}/README.txt"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|