Working on article metadata and list page.
M Cargo.lock +5 -1
@@ 313,7 313,7 @@ dependencies = [
  "rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "semver 0.9.0 (git+https://github.com/steveklabnik/semver.git?rev=27c2b066cc0d3818f8b2047388ce29be2e4c97e1)",
  "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
+ "toml 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]

          
@@ 336,6 336,7 @@ source = "registry+https://github.com/ru
 dependencies = [
  "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 

          
@@ 1866,6 1867,9 @@ source = "registry+https://github.com/ru
 name = "serde"
 version = "1.0.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
+]
 
 [[package]]
 name = "serde_derive"

          
M cf_generate/Cargo.toml +4 -4
@@ 6,17 6,17 @@ edition = "2018"
 
 [dependencies]
 cargofox = {path = "../cargofox", version = "0.2"}
-chrono = "0.4"
+chrono = { version = "0.4", features = ["serde"] }
 diesel = { version = "1.4", features = ["postgres", "extras"] }
 dotenv = "0.13"
 lazy_static = "1"
 log = "0.4"
-serde = "1"
-serde_derive = "1"
+serde = {version = "1", features = ["derive"] }
 rayon = "1"
 semver = {git = "https://github.com/steveklabnik/semver.git", rev = "27c2b066cc0d3818f8b2047388ce29be2e4c97e1", features = ["serde", "diesel"]}
 fs_extra = "1"
 askama = "0.8"
 itertools = "0.8"
 pbr = "1"
-pulldown-cmark = "0.4"
  No newline at end of file
+pulldown-cmark = "0.4"
+toml = "0.5"
  No newline at end of file

          
M cf_generate/articles/0000_hello_world/article.md +6 -1
@@ 41,7 41,12 @@ interesting:
    problem that we've managed to actually make very simple, in the
    basic case.  But there's a heck of a lot of difficulty lurking just
    beneath the surface when you write `foo = '^1.3'`.
- 
+ * Similarly, there's a reason that `crates.io` and `docs.rs` and
+   `crates.rs` all show minimal, if any, information on feature
+   flags.  Turns out that they are *complicated*, and dealing with
+   them in a general-purpose manner involves exploring lots of
+   edge-cases.
+
  * `crates.io` has been attacked already.  I don't mean spamming or
    automated squatting or 
    [somewhat amateurish attempts at activism](https://blog.rust-lang.org/2018/10/19/Update-on-crates.io-incident.html)

          
A => cf_generate/articles/0000_hello_world/meta.toml +3 -0
@@ 0,0 1,3 @@ 
+title = "Hello, world!"
+date = "2019-03-14"
+author = "Simon"

          
M cf_generate/src/main.rs +88 -19
@@ 14,6 14,7 @@ use log::*;
 use pbr::ProgressBar;
 use rayon::prelude::*;
 use semver;
+use toml;
 
 use cargofox::*;
 

          
@@ 22,6 23,7 @@ mod types;
 
 use crate::types::*;
 
+// TODO: Make these all PathBuf instead of String
 lazy_static! {
     /// Directory to output static HTML to.
     static ref OUTPUT_PATH: String = {

          
@@ 41,6 43,25 @@ lazy_static! {
         let s = OUTPUT_PATH.clone() + "/static";
         s
     };
+    /// The path for articles
+    static ref ARTICLE_OUTPUT_PATH: String = {
+        let s = OUTPUT_PATH.clone() + "/articles";
+        s
+    };
+
+    /// The input path for static data
+    static ref STATIC_INPUT_PATH: String = {
+        let s = "cf_generate/static".to_string();
+        s
+    };
+
+
+    /// The input path for articles
+    static ref ARTICLE_INPUT_PATH: String = {
+        let s = "cf_generate/articles".to_string();
+        s
+    };
+
 
 }
 

          
@@ 150,37 171,78 @@ fn output_paginated_list_pages(navlinks:
     }
 }
 
-fn output_blog_index_page(navlinks: &[NavLink]) {
-    let output_file_path: path::PathBuf =
-        [&*OUTPUT_PATH, "articles", "index.html"].iter().collect();
+fn output_article_list_page(navlinks: &[NavLink], article_info: &[(String, ArticleMetadata)]) {
+    let output_file_path: path::PathBuf = [&*ARTICLE_OUTPUT_PATH, "index.html"].iter().collect();
+    info!("Outputting article list page to {:?}", output_file_path);
+    let page = pages::render_article_list_page(navlinks, article_info);
+    let mut output_file = fs::File::create(output_file_path).unwrap();
+    output_file.write_all(page.as_bytes()).unwrap();
 }
 
 // TODO: Copy images and such for article?  hmwrm.
-fn output_blog_article_page(navlinks: &[NavLink], name: &str) {
+fn output_article_page(navlinks: &[NavLink], dir: &str, meta: &ArticleMetadata) {
     use pulldown_cmark as cm;
 
-    let input_file_path: path::PathBuf = ["cf_generate", "articles", name, "article.md"]
-        .iter()
-        .collect();
-    let page_md = "";
+    let input_file_path: path::PathBuf = [&*ARTICLE_INPUT_PATH, dir, "article.md"].iter().collect();
+
+    let page_md = &fs::read_to_string(input_file_path).expect("Could not read article file");
 
     let options = cm::Options::empty();
     let broken_link_callback = |reference: &str, dest: &str| {
-        error!("Broken link in article {}: {} {}", name, reference, dest);
+        error!("Broken link in article {}: {} {}", dir, reference, dest);
         None
     };
     let parser =
         cm::Parser::new_with_broken_link_callback(page_md, options, Some(&broken_link_callback));
     let page_html = &mut String::with_capacity(page_md.len() * 2);
     cm::html::push_html(page_html, parser);
+    let page = pages::render_article_page(navlinks, page_html, meta);
 
-    let output_dir_path: path::PathBuf = [&*OUTPUT_PATH, "articles", name].iter().collect();
-    let output_file_path: path::PathBuf = [&*OUTPUT_PATH, "articles", name, "index.html"]
+    let output_dir_path: path::PathBuf = [&*ARTICLE_OUTPUT_PATH, dir].iter().collect();
+    fs::create_dir_all(&output_dir_path).unwrap();
+    let output_file_path: path::PathBuf =
+        [&*ARTICLE_OUTPUT_PATH, dir, "index.html"].iter().collect();
+    let mut output_file = fs::File::create(output_file_path).unwrap();
+    output_file.write_all(page.as_bytes()).unwrap();
+}
+
+/// Driver for outputting all our blog-ish articles.
+fn output_article_pages(navlinks: &[NavLink]) {
+    let input_dir = path::PathBuf::from(&*ARTICLE_INPUT_PATH);
+    let article_dirs = fs::read_dir(input_dir)
+        .expect("Could not read article dir?")
+        .filter_map(|article_dir| {
+            let article_dir = article_dir.expect("Could not read dir entry?");
+            if !article_dir.path().is_dir() {
+                None
+            } else {
+                // This returns an OsStr, turn it into
+                // a String
+                article_dir.path().file_name().map(|osstr| {
+                    osstr
+                        .to_str()
+                        .expect("Invalid UTF8 in article dir name")
+                        .to_owned()
+                })
+            }
+        });
+    let articles_with_metadata: Vec<(String, ArticleMetadata)> = article_dirs
+        .into_iter()
+        .map(|dir| {
+            let meta_file_path: path::PathBuf =
+                [&*ARTICLE_INPUT_PATH, &dir, "meta.toml"].iter().collect();
+            let meta_toml =
+                &fs::read_to_string(meta_file_path).expect("Could not read article metadata file");
+            let metadata: ArticleMetadata =
+                toml::de::from_str(&meta_toml).expect("Could not parse article metadata?");
+            (dir, metadata)
+        })
+        .collect();
+    info!("Outputting article pages");
+    output_article_list_page(navlinks, articles_with_metadata.as_slice());
+    articles_with_metadata
         .iter()
-        .collect();
-    fs::create_dir_all(&output_dir_path).unwrap();
-    let mut output_file = fs::File::create(output_dir_path).unwrap();
-    output_file.write_all(page_html.as_bytes()).unwrap();
+        .for_each(|(dir, meta)| output_article_page(navlinks, dir, meta));
 }
 
 /// Gets us all the crates, each with all analysis data.

          
@@ 307,12 369,15 @@ fn fetch_full_crates(conn: &PgConn) -> V
 
     // Great, now we actually build things...
     let mut results = Vec::with_capacity(all_crates.len());
+    // For each crate...
     for c in all_crates.iter() {
+        // Collect data for each crate version
         let mut full_versions: Vec<FullCrateVersion> = cv_map
             .get(&c.id)
             .expect("No crate versions for crate?")
             .iter()
             .map(|(cv, ca, dep, meta)| {
+                // Take all our different sources of data and shuffle them together
                 let raw_deps = dep_map.get(&cv.id).cloned().unwrap_or(vec![]);
                 let tokei_analysis = at_map.get(&cv.id).cloned().unwrap_or(vec![]);
 

          
@@ 371,7 436,8 @@ fn fetch_full_crates(conn: &PgConn) -> V
                 }
             })
             .collect();
-        // Sort descending.
+        // Sort descending by version, so each crate index page has
+        // the most recent crate at the top.
         full_versions.sort_by(|x1, x2| x2.version.cmp(&x1.version));
         let latest_version = 0;
         let res = FullCrate {

          
@@ 393,7 459,7 @@ fn fetch_full_crates(conn: &PgConn) -> V
 
 /// The top-level driver function to actually read all the stuff we
 /// want from the DB, template it, and write it out to result files.
-fn generate_crate_pages() {
+fn generate_all_pages() {
     let pool = create_connections_from_env();
     let conn = &mut pool.unwrap().get().unwrap();
 

          
@@ 441,13 507,15 @@ fn generate_crate_pages() {
     // Output other misc stuff.
     output_index_page(navlinks.as_slice());
     output_about_page(navlinks.as_slice());
+    output_article_pages(navlinks.as_slice());
+
     info!("Done!");
 }
 
 /// Copy `static/` directory to output dir.
 fn copy_static_dir() {
     fs_extra::dir::copy(
-        "cf_generate/static/",
+        &*STATIC_INPUT_PATH,
         &*OUTPUT_PATH,
         &fs_extra::dir::CopyOptions::new(),
     )

          
@@ 460,6 528,7 @@ fn copy_static_dir() {
 fn generate_output_dirs() {
     fs::create_dir_all(&*STATIC_OUTPUT_PATH).unwrap();
     fs::create_dir_all(&*CRATE_OUTPUT_PATH).unwrap();
+    fs::create_dir_all(&*ARTICLE_OUTPUT_PATH).unwrap();
     fs::create_dir_all(&*OUTPUT_PATH).unwrap();
 }
 

          
@@ 470,6 539,6 @@ fn main() {
         fs::remove_dir_all(&*OUTPUT_PATH).expect("Could not remove old output?");
     }
     generate_output_dirs();
-    generate_crate_pages();
+    generate_all_pages();
     copy_static_dir();
 }

          
M cf_generate/src/pages.rs +51 -1
@@ 3,7 3,7 @@ 
 
 use askama::Template;
 
-use crate::types::{FullCrate, FullCrateVersion, NavLink};
+use crate::types::*;
 use cargofox::models::{CrateAnalysisTokei, CrateFileAnalysis, DependencyAnalysis};
 
 use std::borrow::Cow;

          
@@ 315,6 315,56 @@ pub fn render_about_page(navlinks: &[Nav
     d.render().unwrap()
 }
 
+#[derive(Template, Clone, Default)]
+#[template(path = "article.html")]
+struct ArticlePageData<'a> {
+    navlinks: Vec<NavLink>,
+    // HTML-rendered contents of the article
+    article: &'a str,
+    title: &'a str,
+    author: &'a str,
+    date: &'a str,
+}
+
+/// Renders the "about" page.  Really just has no dynamic data,
+/// but we want to do it through a template anyway so it matches
+/// everything.
+pub fn render_article_page(navlinks: &[NavLink], article: &str, meta: &ArticleMetadata) -> String {
+    let navlinks = navlinks.to_vec();
+    let d = ArticlePageData {
+        navlinks,
+        article,
+        title: &meta.title,
+        author: &meta.author,
+        date: &meta.date,
+    };
+    d.render().unwrap()
+}
+
+#[derive(Template, Clone, Default)]
+#[template(path = "article_list.html")]
+struct ArticleListPageData<'a> {
+    navlinks: Vec<NavLink>,
+    articles: &'a [&'a str],
+}
+
+/// Renders the "about" page.  Really just has no dynamic data,
+/// but we want to do it through a template anyway so it matches
+/// everything.
+/// TODO: Finish.
+pub fn render_article_list_page(
+    navlinks: &[NavLink],
+    articles: &[(String, ArticleMetadata)],
+) -> String {
+    let navlinks = navlinks.to_vec();
+    let article_dirs: Vec<_> = articles.iter().map(|(d, meta)| d.as_str()).collect();
+    let d = ArticleListPageData {
+        navlinks,
+        articles: article_dirs.as_slice(),
+    };
+    d.render().unwrap()
+}
+
 pub mod filters {
     use chrono::prelude::*;
     use lazy_static::lazy_static;

          
M cf_generate/src/types.rs +13 -0
@@ 3,6 3,7 @@ 
 
 use cargofox::models::{CrateAnalysisTokei, CrateFileAnalysis, DependencyAnalysis};
 use semver;
+use serde::{Deserialize, Serialize};
 
 /// A structure we build containing allll info we have on a crate.
 /// It should let us generate the crate versions index page, plus

          
@@ 80,3 81,15 @@ pub struct NavLink {
     pub start_offset: usize,
     pub end_offset: usize,
 }
+
+/// Metadata for a blog article.
+/// pulldown-cmark doesn't support header metadata sections in pages, so
+/// putting this inline in the markdown is more trouble than it's worth.
+/// We just have a meta.toml file alongside it instead.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct ArticleMetadata {
+    pub title: String,
+    pub author: String,
+    // TODO: Use chrono dates here
+    pub date: String,
+}

          
M cf_generate/templates/article.html +3 -4
@@ 3,9 3,8 @@ 
 {% block title %}Article{% endblock %}
 
 {% block body %}
-Title
+<h1>{{title}}</h1>
+<h2>{{author}} - {{date}}</h2>
 
-Date
-
-Article body goes here.
+{{article|safe}}
 {% endblock %}

          
A => cf_generate/templates/article_list.html +15 -0
@@ 0,0 1,15 @@ 
+{% extends "base.html" %}
+
+{% block title %}Articles{% endblock %}
+
+{% block body %}
+Title
+
+Date
+
+<ul>
+{% for article in articles %}
+<li>{{article}}</li>
+{% endfor %}
+</ul>
+{% endblock %}