Episode guide and trailing slashes in urls
This commit is contained in:
parent
b29ae69a25
commit
573fd8d2a2
|
@ -4,3 +4,6 @@ vendor/
|
|||
dbclones/
|
||||
coverage.out
|
||||
public/media/
|
||||
cinera/*/
|
||||
cinera/cinera.conf
|
||||
annotations/
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
export CINERA_REPO_PATH=/home/handmade/src/handmade-dev/cinera/Annotation-System
|
||||
export CINERA_HMML_PATH=/home/handmade/src/handmade-dev/cinera/cinera_handmade.network
|
||||
export CINERA_ASSETS_PATH=/home/handmade/src/handmade-dev/hmdev/static/annotations
|
||||
export CINERA_OUTPUT_PATH=/home/handmade/src/handmade-dev/annotations
|
||||
export DOMAIN=handmade.local
|
||||
export SCHEME=https
|
||||
export CINERA_MONIT_GROUP=cinera
|
||||
export CINERA_SCRIPT_PATH=/home/handmade/src/handmade-dev/cinera
|
||||
export ANNOTATIONS_USER=handmade
|
||||
|
||||
# NOTE(asaf): Known-working version as of 2021-08-26
|
||||
export CINERA_VERSION=6da970d48ca2cee861b7fe2d8f4d7ed6ca9ccce1
|
|
@ -0,0 +1,74 @@
|
|||
// vim:ft=c:
|
||||
|
||||
include = "$CINERA_HMML_PATH/cinera_includes_hero_people.conf";
|
||||
include = "$CINERA_HMML_PATH/cinera_includes_riscy_people.conf";
|
||||
include = "$CINERA_HMML_PATH/cinera_includes_bitwise_people.conf";
|
||||
|
||||
db_location = "data/cinera.db";
|
||||
cache_dir = "data/cache";
|
||||
|
||||
ignore_privacy = "true";
|
||||
hmml_dir = "$CINERA_HMML_PATH/$owner/$origin/$project";
|
||||
|
||||
base_dir = "$CINERA_OUTPUT_PATH/$lineage";
|
||||
base_url = "$SCHEME://$origin.$DOMAIN";
|
||||
|
||||
assets_root_dir = "$CINERA_ASSETS_PATH";
|
||||
assets_root_url = "$SCHEME://$DOMAIN/public/annotations";
|
||||
default_medium = "programming";
|
||||
|
||||
player_location = "episode/$project";
|
||||
global_theme = "global";
|
||||
theme = "$origin";
|
||||
|
||||
// player_template = "$CINERA_SCRIPT_PATH/cinera_template_episode.html";
|
||||
|
||||
project = "hero" {
|
||||
include = "$CINERA_HMML_PATH/cinera_includes_hero_media.conf";
|
||||
owner = "cmuratori";
|
||||
unit = "Day";
|
||||
title = "";
|
||||
|
||||
project = "misc" {
|
||||
unit = "";
|
||||
title = "Miscellaneous";
|
||||
}
|
||||
project = "intro-to-c" {
|
||||
title = "Introduction to C";
|
||||
}
|
||||
project = "code" {
|
||||
title = "Handmade Hero";
|
||||
}
|
||||
project = "chat" {
|
||||
title = "Handmade Chat";
|
||||
}
|
||||
project = "ray" {
|
||||
title = "Handmade Ray";
|
||||
}
|
||||
}
|
||||
|
||||
project = "riscy" {
|
||||
include = "$CINERA_HMML_PATH/cinera_includes_riscy_media.conf";
|
||||
base_dir = "$CINERA_OUTPUT_PATH/$origin/$project";
|
||||
owner = "miotatsu";
|
||||
unit = "Day";
|
||||
title = "Riscy Business";
|
||||
|
||||
project = "risc" {
|
||||
title = "Riscellaneous";
|
||||
}
|
||||
project = "coad" {
|
||||
title = "Computer Organisation and Architecture";
|
||||
}
|
||||
project = "reader" {
|
||||
title = "Risc-V Book Club";
|
||||
}
|
||||
}
|
||||
|
||||
project = "bitwise" {
|
||||
include = "$CINERA_HMML_PATH/cinera_includes_bitwise_media.conf";
|
||||
base_dir = "$CINERA_OUTPUT_PATH/$origin/$project";
|
||||
owner = "pervognsen";
|
||||
unit = "Day";
|
||||
title = "Bitwise";
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
# How to run Cinera locally
|
||||
|
||||
0. Install prerequisites:
|
||||
a. `libcurl4-openssl-dev`
|
||||
b. `byacc`
|
||||
c. `flex`
|
||||
|
||||
1. Copy `cinera/cinera.conf.sample` to `cinera/cinera.conf` and edit to match your local system
|
||||
2. Run `cinera/user_update_cinera.sh`
|
||||
3. Run `cinera/update_annotations.sh`
|
||||
4. From the cinera dir run: `./run_local.sh`
|
||||
5. Once it's done processing everything (you should see "Monitoring
|
||||
file system for new, edited and deleted .hmml and asset files")
|
||||
You can shut it down with ctrl+c.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
[ -e "./cinera.conf" ] || exit
|
||||
. ./cinera.conf
|
||||
|
||||
$CINERA_REPO_PATH/cinera/cinera -c cinera_hmn.conf
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [ ! -e "cinera.conf" ]; then
|
||||
echo "Can't find cinera.conf"
|
||||
exit
|
||||
fi
|
||||
. cinera.conf
|
||||
|
||||
./update_cinera.sh
|
||||
./update_annotations.sh
|
||||
|
||||
[ -d "data" ] || mkdir data
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
[ -e "./cinera.conf" ] || exit
|
||||
. ./cinera.conf
|
||||
|
||||
nohup $CINERA_REPO_PATH/cinera/cinera -c cinera_hmn.conf > data/cinera.log 2>&1 &
|
||||
echo $! > data/cinera.pid
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
kill $(cat data/cinera.pid)
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [ ! -e "cinera.conf" ]; then
|
||||
echo "Can't find cinera.conf"
|
||||
exit
|
||||
fi
|
||||
. cinera.conf
|
||||
|
||||
if [ ! -d $CINERA_HMML_PATH ]; then
|
||||
git clone git@gitssh.handmade.network:Annotation-Pushers/cinera_handmade.network.git $CINERA_HMML_PATH
|
||||
fi
|
||||
|
||||
if [ ! -d $CINERA_HMML_PATH ]; then
|
||||
echo "Failed to clone annotation repo"
|
||||
exit
|
||||
fi
|
||||
|
||||
cd $CINERA_HMML_PATH
|
||||
git pull
|
||||
|
||||
cp -av $CINERA_HMML_PATH/cmuratori/hero/cinera__hero.css $CINERA_ASSETS_PATH/
|
||||
cp -av $CINERA_HMML_PATH/miotatsu/riscy/cinera__riscy.css $CINERA_ASSETS_PATH/
|
||||
cp -av $CINERA_HMML_PATH/pervognsen/bitwise/cinera__bitwise.css $CINERA_ASSETS_PATH/
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [ ! -e "cinera.conf" ]; then
|
||||
echo "Can't find cinera.conf"
|
||||
exit
|
||||
fi
|
||||
. cinera.conf
|
||||
|
||||
monit -g $CINERA_MONIT_GROUP stop
|
||||
|
||||
CMD="cd $CINERA_SCRIPT_PATH; ./user_update_cinera.sh"
|
||||
|
||||
su - $ANNOTATIONS_USER -c "$CMD"
|
||||
|
||||
monit -g $CINERA_MONIT_GROUP start
|
|
@ -0,0 +1,36 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [ ! -e "cinera.conf" ]; then
|
||||
echo "Can't find cinera.conf"
|
||||
exit
|
||||
fi
|
||||
. cinera.conf
|
||||
|
||||
if [ ! -d $CINERA_REPO_PATH ]; then
|
||||
git clone git@gitssh.handmade.network:Annotation-Pushers/Annotation-System.git $CINERA_REPO_PATH
|
||||
fi
|
||||
|
||||
if [ ! -d $CINERA_REPO_PATH ]; then
|
||||
echo "Failed to clone cinera"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
cd $CINERA_REPO_PATH
|
||||
git pull
|
||||
if [[ -z "${CINERA_VERSION}" ]]; then
|
||||
git checkout master
|
||||
else
|
||||
git checkout $CINERA_VERSION
|
||||
fi
|
||||
cd $CINERA_REPO_PATH/hmmlib
|
||||
make
|
||||
cp hmml.a hmmlib.h ../cinera
|
||||
cd $CINERA_REPO_PATH/cinera
|
||||
`$SHELL cinera.c`
|
||||
|
||||
if [ ! -d $CINERA_ASSETS_PATH ]; then
|
||||
mkdir $CINERA_ASSETS_PATH
|
||||
fi
|
||||
|
||||
cp -av $CINERA_REPO_PATH/cinera/*.{css,js,png} $CINERA_ASSETS_PATH
|
|
@ -0,0 +1,54 @@
|
|||
SET DAEMON 5
|
||||
SET LOGFILE /var/log/monit.log
|
||||
SET STATEFILE /var/lib/monit/state
|
||||
SET HTTPD UNIXSOCKET /var/run/monit.sock
|
||||
allow user:pass
|
||||
SET MAILSERVER
|
||||
box.handmadedev.org
|
||||
PORT 587
|
||||
USERNAME "noreply@handmadedev.org"
|
||||
PASSWORD "[FILL THIS IN]"
|
||||
USING tlsv1
|
||||
SET MAIL-FORMAT {
|
||||
from: noreply@handmadedev.org
|
||||
reply-to: noreply@handmadedev.org
|
||||
subject: $SERVICE $EVENT at $DATE
|
||||
message: Monit $ACTION $SERVICE at $DATE on $HOST: $DESCRIPTION
|
||||
}
|
||||
SET ALERT team@handmadedev.org only on { nonexist, instance }
|
||||
|
||||
CHECK PROCESS beta_cinera PIDFILE /home/hmn-beta/srv/cinera/data/cinera.pid
|
||||
GROUP cinera_beta
|
||||
START PROGRAM = "/home/hmn-beta/srv/cinera/start.sh" AS UID "annotations"
|
||||
STOP PROGRAM = "/home/hmn-beta/srv/cinera/stop.sh" AS UID "annotations"
|
||||
MODE PASSIVE
|
||||
|
||||
CHECK PROCESS live_cinera PIDFILE /home/hmn-live/srv/cinera/data/cinera.pid
|
||||
GROUP cinera_live
|
||||
START PROGRAM = "/home/hmn-live/srv/cinera/start.sh" AS UID "annotations"
|
||||
STOP PROGRAM = "/home/hmn-live/srv/cinera/stop.sh" AS UID "annotations"
|
||||
MODE PASSIVE
|
||||
|
||||
CHECK PROCESS beta_discord_history PIDFILE /home/hmn-beta/discordhistory.pid
|
||||
GROUP discord_history_beta
|
||||
START PROGRAM = "/home/hmn-beta/start_discord_history.sh" AS UID "hmn-beta"
|
||||
STOP PROGRAM = "/bin/bash -c '/bin/kill `/bin/cat /home/hmn-beta/discordhistory.pid`'"
|
||||
MODE ACTIVE
|
||||
|
||||
CHECK PROCESS beta_discord_bot PIDFILE /home/hmn-beta/discordbot.pid
|
||||
GROUP discord_bot_beta
|
||||
START PROGRAM = "/home/hmn-beta/start_discord_bot.sh" AS UID "hmn-beta"
|
||||
STOP PROGRAM = "/bin/bash -c '/bin/kill `/bin/cat /home/hmn-beta/discordbot.pid`'"
|
||||
MODE ACTIVE
|
||||
|
||||
CHECK PROCESS live_discord_history PIDFILE /home/hmn-live/discordhistory.pid
|
||||
GROUP discord_history_live
|
||||
START PROGRAM = "/home/hmn-live/start_discord_history.sh" AS UID "hmn-live"
|
||||
STOP PROGRAM = "/bin/bash -c '/bin/kill `/bin/cat /home/hmn-live/discordhistory.pid`'"
|
||||
MODE ACTIVE
|
||||
|
||||
CHECK PROCESS live_discord_bot PIDFILE /home/hmn-live/discordbot.pid
|
||||
GROUP discord_bot_live
|
||||
START PROGRAM = "/home/hmn-live/start_discord_bot.sh" AS UID "hmn-live"
|
||||
STOP PROGRAM = "/bin/bash -c '/bin/kill `/bin/cat /home/hmn-live/discordbot.pid`'"
|
||||
MODE ACTIVE
|
|
@ -42,4 +42,8 @@ var Config = HMNConfig{
|
|||
AssetsPathPrefix: "",
|
||||
AssetsPublicUrlRoot: "",
|
||||
},
|
||||
EpisodeGuide: EpisodeGuide{
|
||||
CineraOutputPath: "./annotations/",
|
||||
Projects: map[string]string{"hero": "code", "riscy": "riscy", "bitwise": "bitwise"},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ type HMNConfig struct {
|
|||
Email EmailConfig
|
||||
DigitalOcean DigitalOceanConfig
|
||||
Discord DiscordConfig
|
||||
EpisodeGuide EpisodeGuide
|
||||
}
|
||||
|
||||
type PostgresConfig struct {
|
||||
|
@ -76,6 +77,11 @@ type DiscordConfig struct {
|
|||
LibraryChannelID string
|
||||
}
|
||||
|
||||
type EpisodeGuide struct {
|
||||
CineraOutputPath string
|
||||
Projects map[string]string // NOTE(asaf): Maps from slugs to default episode guide topic
|
||||
}
|
||||
|
||||
func (info PostgresConfig) DSN() string {
|
||||
return fmt.Sprintf("user=%s password=%s host=%s port=%d dbname=%s", info.User, info.Password, info.Hostname, info.Port, info.DbName)
|
||||
}
|
||||
|
|
|
@ -276,6 +276,18 @@ func TestLibraryResource(t *testing.T) {
|
|||
AssertSubdomain(t, BuildLibraryResource("hero", 1), "hero")
|
||||
}
|
||||
|
||||
func TestEpisodeGuide(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEpisodeList("hero", ""), RegexEpisodeList, map[string]string{"topic": ""})
|
||||
AssertRegexMatch(t, BuildEpisodeList("hero", "code"), RegexEpisodeList, map[string]string{"topic": "code"})
|
||||
AssertSubdomain(t, BuildEpisodeList("hero", "code"), "hero")
|
||||
|
||||
AssertRegexMatch(t, BuildEpisode("hero", "code", "day001"), RegexEpisode, map[string]string{"topic": "code", "episode": "day001"})
|
||||
AssertSubdomain(t, BuildEpisode("hero", "code", "day001"), "hero")
|
||||
|
||||
AssertRegexMatch(t, BuildCineraIndex("hero", "code"), RegexCineraIndex, map[string]string{"topic": "code"})
|
||||
AssertSubdomain(t, BuildCineraIndex("hero", "code"), "hero")
|
||||
}
|
||||
|
||||
func TestProjectCSS(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
|
||||
}
|
||||
|
|
|
@ -554,6 +554,38 @@ func BuildLibraryResource(projectSlug string, resourceId int) string {
|
|||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
}
|
||||
|
||||
/*
|
||||
* Episode Guide
|
||||
*/
|
||||
|
||||
var RegexEpisodeList = regexp.MustCompile(`^/episode(/(?P<topic>[^/]+))?$`)
|
||||
|
||||
func BuildEpisodeList(projectSlug string, topic string) string {
|
||||
defer CatchPanic()
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString("/episode")
|
||||
if topic != "" {
|
||||
builder.WriteString("/")
|
||||
builder.WriteString(topic)
|
||||
}
|
||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexEpisode = regexp.MustCompile(`^/episode/(?P<topic>[^/]+)/(?P<episode>[^/]+)$`)
|
||||
|
||||
func BuildEpisode(projectSlug string, topic string, episode string) string {
|
||||
defer CatchPanic()
|
||||
return ProjectUrl(fmt.Sprintf("/episode/%s/%s", topic, episode), nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexCineraIndex = regexp.MustCompile(`^/(?P<topic>[^/]+).index$`)
|
||||
|
||||
func BuildCineraIndex(projectSlug string, topic string) string {
|
||||
defer CatchPanic()
|
||||
return ProjectUrl(fmt.Sprintf("/%s.index", topic), nil, projectSlug)
|
||||
}
|
||||
|
||||
/*
|
||||
* Discord OAuth
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{{ template "base.html" . }}
|
||||
|
||||
{{ define "extrahead" }}
|
||||
<link rel="stylesheet" type="text/css" href="{{ static "annotations/cinera.css" }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ static (strjoin "annotations/cinera__" .Project.Subdomain ".css") }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ static "annotations/cinera_topics.css" }}">
|
||||
<script type="text/javascript" src="{{ static "annotations/cinera_pre.js" }}"></script>
|
||||
<script type="text/javascript" src="{{ static "annotations/cinera_post.js" }}" defer></script>
|
||||
<script type="text/javascript" src="{{ static "annotations/cinera_player_pre.js" }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="content-block">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
{{ end }}
|
|
@ -0,0 +1,111 @@
|
|||
{{ template "base.html" . }}
|
||||
|
||||
{{ define "extrahead" }}
|
||||
<style type="text/css">
|
||||
.queryContainer {
|
||||
width: 80%;
|
||||
margin: 15px auto 20px auto;
|
||||
display: flex;
|
||||
flex-direction: horizontal;
|
||||
}
|
||||
|
||||
.queryContainer input {
|
||||
height:3em;
|
||||
}
|
||||
|
||||
.queryContainer #query {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.queryContainer #submit_button {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
padding:0px 10px;
|
||||
vertical-align:middle;
|
||||
margin:0px;
|
||||
}
|
||||
#annotationContainer {
|
||||
left: -10px;
|
||||
margin: 0px;
|
||||
margin-left: -20px;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px transparent none;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static "annotations/cinera.css" }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ static (strjoin "annotations/cinera__" .Project.Subdomain ".css") }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ static "annotations/cinera_topics.css" }}">
|
||||
<script type="text/javascript" src="{{ static "annotations/cinera_pre.js" }}"></script>
|
||||
<script type="text/javascript" src="{{ static "annotations/cinera_post.js" }}" defer></script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-column flex-row-l">
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<h2>Annotations for the topic: {{ .CurrentTopic }}</h2>
|
||||
{{ .Content }}
|
||||
</div>
|
||||
<div class="sidebar flex-shrink-0 mw6 w-30-l self-center self-start-l mh3 mh0-ns ml3-l overflow-hidden">
|
||||
<div class="content-block box logo">
|
||||
<img alt="{{ .Project.Name }}" src="{{ .Project.Logo }}">
|
||||
</div>
|
||||
<div class="content-block subprojects">
|
||||
<strong>Also have a look at these {{ .Project.Name }} annotations:</strong>
|
||||
<ul>
|
||||
{{ range .Topics }}
|
||||
{{ if .Url }}
|
||||
<li class="ttc">
|
||||
<a href="{{ .Url }}">{{ .LinkText }}</a>
|
||||
</li>
|
||||
{{ else }}
|
||||
<li class="ttc"><strong>{{ .LinkText }}</strong></li>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ if eq .Project.Subdomain "hero" }}
|
||||
<div class="content-block donate">
|
||||
<br/>
|
||||
<strong>Preorder the Game, Get the Source!</strong>
|
||||
<p>Visit <a target="_blank" href="https://handmadehero.org">https://handmadehero.org</a> for more information about Handmade Hero</p>
|
||||
<br/>
|
||||
<strong>Support the Episode Guide</strong>
|
||||
<p><strong><a href="https://handmade.network/m/Miblo">Miblo</a></strong> is currently the sole maintainer of this episode guide. You can see him working live on
|
||||
<a class="external" href="http://www.twitch.tv/miblo" target="_blank">Twitch</a>.</p>
|
||||
</div>
|
||||
<div class="content-block">
|
||||
<br/>
|
||||
<p><strong>Credits</strong></p>
|
||||
<p style="text-align: left; ">
|
||||
The detailed episode guides linked to on this page would also not have been possible without the generous historical contributions of time and energy from:
|
||||
<ul style="text-align: left;">
|
||||
<li>Jace Bennett (<a href="https://twitter.com/jacebennett" target="_blank">@jacebennett</a>),</li>
|
||||
<li>Abner Coimbre (<a href="https://twitter.com/AbnerCoimbre" target="_blank">@AbnerCoimbre</a>),</li>
|
||||
<li>Matthew VanDevander (<a href="https://twitter.com/mvandevander" target="_blank">@mvandevander</a></li>
|
||||
<li>Gustavo Chávez (<a href="https://twitter.com/gusChvz" target="_blank">@gusChvz</a>),</li>
|
||||
<li>Kasper Sauramo (<a href="https://twitter.com/KMSchme" target="_blank">@KMSchme</a>),</li>
|
||||
<li>Ben Craddock (<a href="https://twitter.com/theinternetftw" target="_blank">@theinternetftw</a>),</li>
|
||||
<li>Dustin Specht (<a href="https://twitter.com/Dustin_Specht" target="_blank">@Dustin_Specht</a>),</li>
|
||||
<li>Jacob Pike (<a href="https://twitter.com/pikejd" target="_blank">@pikejd</a>),</li>
|
||||
<li>Matt Mascarenhas (<a href="https://twitter.com/miblo_" target="_blank">@miblo_</a>),</li>
|
||||
<li>Miguel Lechón (<a href="https://twitter.com/debiatan" target="_blank">@debiatan</a>),</li>
|
||||
<li>Cory Henderlite (<a href="https://twitter.com/effect0r" target="_blank">@effect0r</a>),</li>
|
||||
<li>Insofaras (<a href="https://twitter.com/insofaras_" target="_blank">@insofaras_</a>),</li>
|
||||
<li>Clay Murray (<a href="https://twitter.com/powerc9000" target="_blank">@powerc9000</a>) and</li>
|
||||
<li>Tim Liou (<a href="https://twitter.com/wdliou" target="_blank">@wdliou</a>).</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>Please take a moment to thank our contributors if you happen to see them on The Twitters!</p>
|
||||
</div>
|
||||
{{ else if eq .Project.Subdomain "riscy" }}
|
||||
<div class="content-block donate">
|
||||
<strong>Support the Episode Guide</strong>
|
||||
<p><strong><a href="https://handmade.network/m/Miblo">Miblo</a></strong> is currently the sole maintainer of this episode guide. You can see him working live on
|
||||
<a class="external" href="http://www.twitch.tv/miblo" target="_blank">Twitch</a>.</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
|
@ -58,7 +58,7 @@
|
|||
<a href="{{ .Header.ManifestoUrl }}" class="misson">Mission</a>
|
||||
{{ end }}
|
||||
{{/* {% if project.default_annotation_category %} */}}
|
||||
{{ if false }}
|
||||
{{ if .Header.EpisodeGuideUrl }}
|
||||
<a href="{{ .Header.EpisodeGuideUrl }}" class="annotations">Episode Guide</a>
|
||||
{{ end }}
|
||||
{{ if .Header.EditUrl }}
|
||||
|
|
|
@ -105,6 +105,9 @@ var HMNTemplateFuncs = template.FuncMap{
|
|||
}
|
||||
return a
|
||||
},
|
||||
"strjoin": func(strs ...string) string {
|
||||
return strings.Join(strs, "")
|
||||
},
|
||||
"absolutedate": func(t time.Time) string {
|
||||
return t.UTC().Format("January 2, 2006, 3:04pm")
|
||||
},
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
package website
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
)
|
||||
|
||||
func CineraIndex(c *RequestContext) ResponseData {
|
||||
topic := c.PathParams["topic"]
|
||||
slug := c.CurrentProject.Slug
|
||||
|
||||
_, foundTopic := topicsForProject(slug, topic)
|
||||
if foundTopic == "" {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
indexPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, slug, foundTopic, fmt.Sprintf("%s.index", foundTopic))
|
||||
|
||||
content, err := os.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
res.Write(content)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
var episodeListRegex = regexp.MustCompile(`(?ism)^.*<body>(?P<guide>.*)</body>.*$`)
|
||||
|
||||
type EpisodeListData struct {
|
||||
templates.BaseData
|
||||
Content template.HTML
|
||||
CurrentTopic string
|
||||
Topics []templates.Link
|
||||
}
|
||||
|
||||
func EpisodeList(c *RequestContext) ResponseData {
|
||||
topic := c.PathParams["topic"]
|
||||
slug := c.CurrentProject.Slug
|
||||
|
||||
defaultTopic, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug]
|
||||
|
||||
if !hasEpisodeGuide {
|
||||
return c.Redirect(hmnurl.BuildProjectHomepage(slug), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
if topic == "" {
|
||||
return c.Redirect(hmnurl.BuildEpisodeList(slug, defaultTopic), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
allTopics, foundTopic := topicsForProject(slug, topic)
|
||||
if foundTopic == "" {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
htmlPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, slug, foundTopic, "index.html")
|
||||
|
||||
htmlContent, err := os.ReadFile(htmlPath)
|
||||
if err != nil {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
matches := episodeListRegex.FindStringSubmatch(string(htmlContent))
|
||||
if matches == nil || matches[episodeListRegex.SubexpIndex("guide")] == "" {
|
||||
c.Logger.Error().Str("Filename", htmlPath).Msg("Episode guide index.html can't be parsed.")
|
||||
return FourOhFour(c)
|
||||
}
|
||||
guide := matches[episodeListRegex.SubexpIndex("guide")]
|
||||
|
||||
var topicLinks []templates.Link
|
||||
for _, t := range allTopics {
|
||||
url := ""
|
||||
if t != foundTopic {
|
||||
url = hmnurl.BuildEpisodeList(slug, t)
|
||||
}
|
||||
topicLinks = append(topicLinks, templates.Link{LinkText: t, Url: url})
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
baseData := getBaseData(c)
|
||||
res.MustWriteTemplate("episode_list.html", EpisodeListData{
|
||||
BaseData: baseData,
|
||||
Content: template.HTML(guide),
|
||||
CurrentTopic: foundTopic,
|
||||
Topics: topicLinks,
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
var episodeTitleRegex = regexp.MustCompile(`(?ism)<span class="episode_name">(?P<title>.*?)</span>`)
|
||||
var episodeContentRegex = regexp.MustCompile(`(?ism)<body>(?P<content>.*)</body>`)
|
||||
|
||||
type EpisodeData struct {
|
||||
templates.BaseData
|
||||
Content template.HTML
|
||||
}
|
||||
|
||||
func Episode(c *RequestContext) ResponseData {
|
||||
episode := c.PathParams["episode"]
|
||||
topic := c.PathParams["topic"]
|
||||
slug := c.CurrentProject.Slug
|
||||
|
||||
_, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug]
|
||||
|
||||
if !hasEpisodeGuide {
|
||||
return c.Redirect(hmnurl.BuildProjectHomepage(slug), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
_, foundTopic := topicsForProject(slug, topic)
|
||||
if foundTopic == "" {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
foundEpisode := findEpisode(slug, foundTopic, episode)
|
||||
if foundEpisode == "" {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
htmlPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, slug, foundTopic, "episode", foundTopic, foundEpisode, "index.html")
|
||||
htmlContent, err := os.ReadFile(htmlPath)
|
||||
if err != nil {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
titleMatches := episodeTitleRegex.FindStringSubmatch(string(htmlContent))
|
||||
title := fmt.Sprintf("%s Episode Guide", c.CurrentProject.Name)
|
||||
if titleMatches != nil && titleMatches[episodeTitleRegex.SubexpIndex("title")] != "" {
|
||||
title = fmt.Sprintf("%s | %s", titleMatches[episodeTitleRegex.SubexpIndex("title")], title)
|
||||
}
|
||||
|
||||
contentMatches := episodeContentRegex.FindStringSubmatch(string(htmlContent))
|
||||
if contentMatches == nil || contentMatches[episodeContentRegex.SubexpIndex("content")] == "" {
|
||||
c.Logger.Error().Str("filename", htmlPath).Msg("Episode file can't be parsed.")
|
||||
return FourOhFour(c)
|
||||
}
|
||||
content := contentMatches[episodeContentRegex.SubexpIndex("content")]
|
||||
|
||||
var res ResponseData
|
||||
baseData := getBaseData(c)
|
||||
baseData.Title = title
|
||||
res.MustWriteTemplate("episode.html", EpisodeData{
|
||||
BaseData: baseData,
|
||||
Content: template.HTML(content),
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func topicsForProject(projectSlug string, requestedTopic string) ([]string, string) {
|
||||
searchPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, projectSlug)
|
||||
entries, err := fs.ReadDir(os.DirFS(searchPath), ".")
|
||||
if err != nil {
|
||||
return nil, ""
|
||||
}
|
||||
var allTopics []string
|
||||
foundTopic := ""
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
t := entry.Name()
|
||||
allTopics = append(allTopics, t)
|
||||
if strings.ToLower(t) == strings.ToLower(requestedTopic) {
|
||||
foundTopic = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allTopics, foundTopic
|
||||
}
|
||||
|
||||
// NOTE(asaf): Assuming topic is valid. Please verify before calling.
|
||||
func findEpisode(projectSlug string, topic string, requestedEpisode string) string {
|
||||
searchPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, projectSlug, topic, "episode", topic) // NOTE(asaf): Yes. We have `topic` twice in the path.
|
||||
entries, err := fs.ReadDir(os.DirFS(searchPath), ".")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
episode := entry.Name()
|
||||
if strings.ToLower(episode) == strings.ToLower(requestedEpisode) {
|
||||
return episode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
|
@ -72,6 +72,11 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||
continue
|
||||
}
|
||||
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
match := route.Regex.FindStringSubmatch(path)
|
||||
if match == nil {
|
||||
continue
|
||||
|
|
|
@ -192,6 +192,13 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
|
|||
mainRoutes.GET(hmnurl.RegexUserProfile, UserProfile)
|
||||
mainRoutes.GET(hmnurl.RegexProjectNotApproved, ProjectHomepage)
|
||||
|
||||
mainRoutes.GET(hmnurl.RegexDiscordOAuthCallback, authMiddleware(DiscordOAuthCallback))
|
||||
mainRoutes.POST(hmnurl.RegexDiscordUnlink, authMiddleware(csrfMiddleware(DiscordUnlink)))
|
||||
mainRoutes.POST(hmnurl.RegexDiscordShowcaseBacklog, authMiddleware(csrfMiddleware(DiscordShowcaseBacklog)))
|
||||
|
||||
mainRoutes.GET(hmnurl.RegexUserSettings, authMiddleware(UserSettings))
|
||||
mainRoutes.POST(hmnurl.RegexUserSettings, authMiddleware(csrfMiddleware(UserSettingsSave)))
|
||||
|
||||
// NOTE(asaf): Any-project routes:
|
||||
mainRoutes.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread))
|
||||
mainRoutes.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit)))
|
||||
|
@ -228,12 +235,9 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
|
|||
mainRoutes.GET(hmnurl.RegexPodcastEpisode, PodcastEpisode)
|
||||
mainRoutes.GET(hmnurl.RegexPodcastRSS, PodcastRSS)
|
||||
|
||||
mainRoutes.GET(hmnurl.RegexDiscordOAuthCallback, authMiddleware(DiscordOAuthCallback))
|
||||
mainRoutes.POST(hmnurl.RegexDiscordUnlink, authMiddleware(csrfMiddleware(DiscordUnlink)))
|
||||
mainRoutes.POST(hmnurl.RegexDiscordShowcaseBacklog, authMiddleware(csrfMiddleware(DiscordShowcaseBacklog)))
|
||||
|
||||
mainRoutes.GET(hmnurl.RegexUserSettings, authMiddleware(UserSettings))
|
||||
mainRoutes.POST(hmnurl.RegexUserSettings, authMiddleware(csrfMiddleware(UserSettingsSave)))
|
||||
mainRoutes.GET(hmnurl.RegexEpisodeList, EpisodeList)
|
||||
mainRoutes.GET(hmnurl.RegexEpisode, Episode)
|
||||
mainRoutes.GET(hmnurl.RegexCineraIndex, CineraIndex)
|
||||
|
||||
mainRoutes.GET(hmnurl.RegexProjectCSS, ProjectCSS)
|
||||
mainRoutes.GET(hmnurl.RegexEditorPreviewsJS, func(c *RequestContext) ResponseData {
|
||||
|
@ -261,6 +265,12 @@ func getBaseData(c *RequestContext) templates.BaseData {
|
|||
|
||||
notices := getNoticesFromCookie(c)
|
||||
|
||||
episodeGuideUrl := ""
|
||||
defaultTopic, hasAnnotations := config.Config.EpisodeGuide.Projects[c.CurrentProject.Slug]
|
||||
if hasAnnotations {
|
||||
episodeGuideUrl = hmnurl.BuildEpisodeList(c.CurrentProject.Slug, defaultTopic)
|
||||
}
|
||||
|
||||
return templates.BaseData{
|
||||
Theme: c.Theme,
|
||||
|
||||
|
@ -289,7 +299,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
|
|||
ForumsUrl: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1),
|
||||
LibraryUrl: hmnurl.BuildLibrary(c.CurrentProject.Slug),
|
||||
ManifestoUrl: hmnurl.BuildManifesto(),
|
||||
EpisodeGuideUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||
EpisodeGuideUrl: episodeGuideUrl,
|
||||
EditUrl: "",
|
||||
SearchActionUrl: "https://duckduckgo.com",
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue