SDL: examples: added homepage + categories pages + added CSS (similar to wiki)

From 56da4e81d8dae199bffd654d4e7ea98ce8b24ae8 Mon Sep 17 00:00:00 2001
From: Nicolas Allemand <[EMAIL REDACTED]>
Date: Wed, 4 Dec 2024 12:12:43 +0100
Subject: [PATCH] examples: added homepage + categories pages + added CSS
 (similar to wiki)

---
 build-scripts/build-web-examples.pl | 105 +++++++++-
 examples/template-category.html     |  30 +++
 examples/template-homepage.html     |  32 ++++
 examples/template.css               | 284 ++++++++++++++++++++++++++++
 examples/template.html              | 114 ++++++++---
 5 files changed, 532 insertions(+), 33 deletions(-)
 create mode 100644 examples/template-category.html
 create mode 100644 examples/template-homepage.html
 create mode 100644 examples/template.css

diff --git a/build-scripts/build-web-examples.pl b/build-scripts/build-web-examples.pl
index 0053295e9f60b..6df912aa8c7c3 100755
--- a/build-scripts/build-web-examples.pl
+++ b/build-scripts/build-web-examples.pl
@@ -71,6 +71,38 @@ sub build_latest {
     }
 }
 
+sub get_categories {
+    my @categories = ();
+
+    opendir(my $dh, $examples_dir) or die("Couldn't opendir '$examples_dir': $!\n");
+    foreach my $dir (sort readdir $dh) {
+        next if ($dir eq '.') || ($dir eq '..');  # obviously skip current and parent entries.
+        next if not -d "$examples_dir/$dir";   # only care about subdirectories.
+
+        push @categories, $dir;
+    }
+    closedir($dh);
+
+    return @categories;
+}
+
+sub get_examples_for_category {
+    my $category = shift;
+
+    my @examples = ();
+
+    opendir(my $dh, "$examples_dir/$category") or die("Couldn't opendir '$examples_dir/$category': $!\n");
+    foreach my $dir (sort readdir $dh) {
+        next if ($dir eq '.') || ($dir eq '..');  # obviously skip current and parent entries.
+        next if not -d "$examples_dir/$category/$dir";   # only care about subdirectories.
+
+        push @examples, $dir;
+    }
+    closedir($dh);
+
+    return @examples;
+}
+
 sub handle_example_dir {
     my $category = shift;
     my $example = shift;
@@ -146,6 +178,11 @@ sub handle_example_dir {
 
     waitpid($pid, 0);
 
+    my $other_examples_html = "<ul>";
+    foreach my $example (get_examples_for_category($category)) {
+        $other_examples_html .= "<li><a href='/$category/$example'>$category/$example</a></li>";
+    }
+    $other_examples_html .= "</ul>";
 
     my $html = '';
     open my $htmltemplate, '<', "$examples_dir/template.html" or die("Couldn't open '$examples_dir/template.html': $!\n");
@@ -156,6 +193,7 @@ sub handle_example_dir {
         s/\@javascript_file\@/$jsfname/g;
         s/\@htmlified_source_code\@/$htmlified_source_code/g;
         s/\@description\@/$description/g;
+        s/\@other_examples_html\@/$other_examples_html/g;
         $html .= $_;
     }
     close($htmltemplate);
@@ -168,8 +206,6 @@ sub handle_example_dir {
 sub handle_category_dir {
     my $category = shift;
 
-    # !!! FIXME: this needs to generate a preview page for all the examples things in the category.
-
     print("Category $category ...\n");
 
     do_mkdir("$output_dir/$category");
@@ -183,6 +219,35 @@ sub handle_category_dir {
     }
 
     closedir($dh);
+
+    my $examples_list_html = "";
+    foreach my $example (get_examples_for_category($category)) {
+        # !!! FIXME: image
+        my $example_image_url = "https://placehold.co/600x400/png";
+        $examples_list_html .= "
+        <a href='/$category/$example'>
+          <div>
+            <img src='$example_image_url' />
+            <div>$category/$example</div>
+          </div>
+        </a>";
+    }
+
+    # write category page
+    my $dst = "$output_dir/$category";
+    my $html = '';
+    open my $htmltemplate, '<', "$examples_dir/template-category.html" or die("Couldn't open '$examples_dir/template-category.html': $!\n");
+    while (<$htmltemplate>) {
+        s/\@project_name\@/$project/g;
+        s/\@category_name\@/$category/g;
+        s/\@examples_list_html\@/$examples_list_html/g;
+        $html .= $_;
+    }
+    close($htmltemplate);
+
+    open my $htmloutput, '>', "$dst/index.html" or die("Couldn't open '$dst/index.html': $!\n");
+    print $htmloutput $html;
+    close($htmloutput);
 }
 
 
@@ -210,6 +275,8 @@ sub handle_category_dir {
 
 build_latest();
 
+do_copy("$examples_dir/template.css", "$output_dir/examples.css");
+
 opendir(my $dh, $examples_dir) or die("Couldn't opendir '$examples_dir': $!\n");
 
 while (readdir($dh)) {
@@ -221,6 +288,38 @@ sub handle_category_dir {
 
 closedir($dh);
 
+# write homepage
+my $homepage_list_html = "";
+foreach my $category (get_categories()) {
+    $homepage_list_html .= "<h2>$category</h2>";
+    $homepage_list_html .= "<div class='list'>";
+    foreach my $example (get_examples_for_category($category)) {
+        # !!! FIXME: image
+        my $example_image_url = "https://placehold.co/600x400/png";
+        $homepage_list_html .= "
+            <a href='/$category/$example'>
+            <div>
+                <img src='$example_image_url' />
+                <div>$category/$example</div>
+            </div>
+            </a>";
+    }
+    $homepage_list_html .= "</div>";
+}
+
+my $dst = "$output_dir/";
+my $html = '';
+open my $htmltemplate, '<', "$examples_dir/template-homepage.html" or die("Couldn't open '$examples_dir/template-category.html': $!\n");
+while (<$htmltemplate>) {
+    s/\@project_name\@/$project/g;
+    s/\@homepage_list_html\@/$homepage_list_html/g;
+    $html .= $_;
+}
+close($htmltemplate);
+
+open my $htmloutput, '>', "$dst/index.html" or die("Couldn't open '$dst/index.html': $!\n");
+print $htmloutput $html;
+close($htmloutput);
+
 print("All examples built successfully!\n");
 exit(0);  # success!
-
diff --git a/examples/template-category.html b/examples/template-category.html
new file mode 100644
index 0000000000000..bc79e8d834a8d
--- /dev/null
+++ b/examples/template-category.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>@project_name@ Examples: @category_name@</title>
+    <link rel="stylesheet" type="text/css" href="/examples.css" />
+    <style>
+      main > h1 {
+        margin-top: 0;
+      }
+    </style>
+  </head>
+  <body>
+    <header>
+      <a href="/">SDL Examples</a>
+    </header>
+    <main>
+      <nav class="breadcrumb">
+        <ul>
+          <li><a href="/">@project_name@</a></li>
+          <li><a href="/@category_name@">@category_name@</a></li>
+        </ul>
+      </nav>
+      <h1>@project_name@ examples: @category_name@</h1>
+      <div class="list">@examples_list_html@</div>
+    </main>
+  </body>
+</html>
diff --git a/examples/template-homepage.html b/examples/template-homepage.html
new file mode 100644
index 0000000000000..369237df9e460
--- /dev/null
+++ b/examples/template-homepage.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>@project_name@ Examples</title>
+    <link rel="stylesheet" type="text/css" href="/examples.css" />
+    <style>
+      main > h1 {
+        margin-top: 0;
+      }
+    </style>
+  </head>
+  <body>
+    <header>
+      <a href="/">SDL Examples</a>
+    </header>
+    <main>
+      <nav class="breadcrumb">
+        <ul>
+          <li><a href="/">@project_name@</a></li>
+        </ul>
+      </nav>
+      <h1>@project_name@ examples</h1>
+
+      <p>Check out the @project_name@ examples here!</p>
+
+      @homepage_list_html@
+    </main>
+  </body>
+</html>
diff --git a/examples/template.css b/examples/template.css
new file mode 100644
index 0000000000000..8e86723cd30aa
--- /dev/null
+++ b/examples/template.css
@@ -0,0 +1,284 @@
+/** from ghwikipp.css */
+:root {
+  color-scheme: dark light; /* both supported */
+}
+
+body {
+  background-color: white;
+  padding: 2vw;
+  color: #333;
+  max-width: 1200px;
+  margin: 0 auto;
+  font-size: 16px;
+  line-height: 1.5;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
+    Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+  overflow-wrap: break-word;
+}
+
+a {
+  color: #0969da;
+  /* text-decoration: none; */
+}
+
+a:visited {
+  color: #064998;
+}
+
+h1 {
+  border-bottom: 2px solid #efefef;
+}
+
+h2 {
+  border-bottom: 1px solid #efefef;
+}
+
+p {
+  max-width: 85ch;
+}
+
+li {
+  max-width: 85ch;
+}
+
+div.sourceCode {
+  background-color: #f6f8fa;
+  max-width: 100%;
+  padding: 16px;
+}
+
+code {
+  background-color: #f6f8fa;
+  padding: 0px;
+  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
+    "Liberation Mono", monospace;
+}
+
+table {
+  border: 1px solid #808080;
+  border-collapse: collapse;
+}
+
+td {
+  border: 1px solid #808080;
+  padding: 5px;
+}
+
+tr:nth-child(even) {
+  background-color: #f6f8fa;
+}
+
+.wikitopbanner {
+  background-color: #efefef;
+  padding: 10px;
+  margin-bottom: 10px;
+  width: auto;
+}
+
+.wikibottombanner {
+  background-color: #efefef;
+  padding: 10px;
+  margin-top: 10px;
+  width: auto;
+}
+
+.alertBox {
+  background-color: #f8d7da;
+  border: 1px solid #f5c6cb;
+  max-width: 60%;
+  padding: 10;
+  margin: auto;
+}
+
+.anchorImage {
+  visibility: hidden;
+  padding-left: 0.2em;
+  color: #fff;
+}
+
+.anchorText:hover .anchorImage {
+  visibility: visible;
+}
+
+hr {
+  display: block;
+  height: 1px;
+  border: 0;
+  border-top: 1px solid #efefef;
+  margin: 1em 0;
+  padding: 0;
+}
+
+/* Text and background color for dark mode */
+@media (prefers-color-scheme: dark) {
+  body {
+    color: #e6edf3;
+    background-color: #0d1117;
+  }
+
+  h1 {
+    border-color: rgba(48, 54, 61, 0.7);
+  }
+
+  h2 {
+    border-color: rgba(48, 54, 61, 0.7);
+  }
+
+  hr {
+    border-color: rgba(48, 54, 61, 0.7);
+  }
+
+  div.sourceCode {
+    background-color: #161b22;
+  }
+
+  code {
+    background-color: #161b22;
+  }
+
+  a {
+    color: #4493f8;
+  }
+
+  a:visited {
+    color: #2f66ad;
+  }
+
+  table {
+    border-color: rgba(48, 54, 61, 0.7);
+  }
+
+  td {
+    border-color: rgba(48, 54, 61, 0.7);
+  }
+
+  tr:nth-child(even) {
+    background-color: #161b22;
+  }
+
+  .wikitopbanner {
+    background-color: #263040;
+  }
+
+  .wikibottombanner {
+    background-color: #263040;
+  }
+
+  .anchorText:hover .anchorImage {
+    filter: invert(100%);
+  }
+}
+
+@media print {
+  body {
+    font-size: 12px;
+  }
+
+  table {
+    font-size: inherit;
+  }
+
+  a:visited {
+    color: #0969da;
+  }
+
+  .wikitopbanner,
+  .anchorText,
+  .wikibottombanner {
+    display: none;
+  }
+}
+
+/** additional (& overrides) for examples */
+header {
+  background-color: #efefef;
+  padding: 10px;
+  font-size: 2rem;
+}
+
+header > a,
+header > a:hover,
+header > a:visited {
+  color: inherit;
+  text-decoration: none;
+}
+
+.breadcrumb {
+  padding: 0.75rem 0.75rem;
+}
+
+.breadcrumb ul {
+  display: flex;
+  flex-wrap: wrap;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+.breadcrumb li:not(:last-child)::after {
+  display: inline-block;
+  margin: 0 0.25rem;
+  content: "ยป";
+}
+
+.list {
+  display: flex;
+  flex-flow: row wrap;
+  gap: 24px;
+}
+
+.list > a > div {
+  width: 200px;
+  border: 5px solid #efefef;
+  border-radius: 5px;
+  background: #efefef;
+
+  display: flex;
+  flex-flow: column nowrap;
+
+  transition: border 0.25s;
+}
+
+.list > a > div:hover {
+  border-color: #064998;
+}
+
+.list > a > div > img {
+  width: 100%;
+  border-radius: 5px;
+}
+
+.list > a > div > div {
+  text-align: center;
+}
+
+.list > a,
+.list > a:visited {
+  display: block;
+  color: inherit;
+  text-decoration: none;
+}
+.list > a:hover {
+  color: #0969da;
+}
+
+@media (prefers-color-scheme: dark) {
+  header {
+    background-color: #263040;
+  }
+
+  .breadcrumb li:not(:last-child)::after {
+    color: #efefef;
+  }
+
+  .list > a > div {
+    border-color: #333;
+    background: #333;
+  }
+}
+
+@media only screen and (max-width: 992px) {
+  .list > a > div {
+    width: 150px;
+  }
+}
diff --git a/examples/template.html b/examples/template.html
index e8be678b31fe5..caf6dccca456c 100644
--- a/examples/template.html
+++ b/examples/template.html
@@ -3,27 +3,48 @@
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
     <title>@project_name@ Example: @category_name@/@example_name@</title>
+    <link rel="stylesheet" type="text/css" href="/examples.css" />
     <style>
-      html, body {
-        width: 100vw;
-        height: 100vh;
-        overflow: hidden;
-        font-family: 'Liberation Sans', sans-serif;
+      main {
+        display: flex;
       }
 
-      .canvas-container {
-        position: absolute;
-        top: 0;
-        left: 0;
-        right: 0;
-        bottom: 0;
+      main > #sidebar {
+        flex: 0 1 25%;
+        border-left: 2px solid #efefef;
+        padding: 1rem 1rem;
+      }
+
+      main > #content {
+        flex: 1 1 auto;
+        margin-bottom: 16px;
+      }
+
+      main > #content > h1 {
+        margin-top: 0;
+      }
+
+      main > #sidebar ul {
+        list-style-type: none;
+        padding: 0;
+        margin: 0;
+      }
 
+      main > #sidebar li {
+        padding: 2px 0;
+      }
+
+      #example-description {
+        max-width: 85ch;
+        margin-bottom: 16px;
+      }
+
+      .canvas-container {
         display: flex;
         align-items: center;
         justify-content: center;
-
-        background: black;
       }
 
       #canvas {
@@ -31,7 +52,7 @@
       }
 
       #output-container {
-        position: absolute;
+        position: fixed;
         top: 100%;
         left: 0;
         right: 0;
@@ -47,8 +68,8 @@
       }
 
       #output-container::before {
-        position: absolute;
-        bottom: 100%;
+        position: fixed;
+        bottom: 0;
         right: 1rem;
 
         content: 'Console';
@@ -87,11 +108,12 @@
         outline: none;
         resize: none;
 
-        font-family: 'Lucida Console', Monaco, monospace;
+        font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
+          "Liberation Mono", monospace;
       }
 
       #source-code {
-        position: absolute;
+        position: fixed;
         top: 100%;
         left: 0;
         right: 0;
@@ -104,8 +126,8 @@
       }
 
       #source-code::before {
-        position: absolute;
-        bottom: 100%;
+        position: fixed;
+        bottom: 0;
         left: 1rem;
 
         content: 'Source code';
@@ -134,21 +156,53 @@
         overflow: scroll;
       }
 
-      #example-description {
-        color: white;
-        text-align: center;
-        position: relative; /* required for proper positioning */
+      @media (prefers-color-scheme: dark) {
+        main > #sidebar {
+          border-color: rgba(48, 54, 61, 0.7);
+        }
+      }
+
+      @media only screen and (max-width: 992px) {
+        main {
+          flex-direction: column;
+        }
+
+        main > #sidebar {
+          border: none;
+        }
       }
     </style>
     <link rel="stylesheet" type="text/css" href="highlight.css">
   </head>
   <body>
-    <div class="canvas-container">
-      <canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
-    </div>
-    <div id="example-description">
-      @description@
-    </div>
+    <header>
+      <a href="/">SDL Examples</a>
+    </header>
+    <main>
+      <div id="content">
+        <nav class="breadcrumb">
+          <ul>
+            <li><a href="/">@project_name@</a></li>
+            <li><a href="/@category_name@">@category_name@</a></li>
+            <li><a href="/@category_name@/@example_name@">@example_name@</a></li>
+          </ul>
+        </nav>
+        <h1>@project_name@ example: @category_name@/@example_name@</h1>
+        <div id="example-description">@description@</div>
+        <div class="canvas-container">
+          <canvas
+            id="canvas"
+            oncontextmenu="event.preventDefault()"
+            tabindex="-1"
+          ></canvas>
+        </div>
+      </div>
+      <div id="sidebar">
+        <h3>Other examples:</h3>
+        @other_examples_html@
+      </div>
+    </main>
+
     <div id="output-container">
       <textarea id="output" rows="8" spellcheck="false" readonly></textarea>
     </div>