Skip to content

Commit

Permalink
feat(liquid): Make page variables future-proof
Browse files Browse the repository at this point in the history
This is a part of #257

Attributes from a page are now under the `page` object
- `path` -> `page.permalink` to make it clear this is the equivelant of `permalink` in the frontmatter
- `source` -> `page.file.permalink`
  - Under `file` to make room for `page.file.parent`
  - `permalink for consitency with `page.permalink`
- `date` -> `page.published_date`
  - Consistency with the new `published_date` in the front matter
  - Both are renamed to make it clear the role of the date (versus a future `modified_date` that might exist)
- `draft` -> `page.is_draft`
- `previous` -> `page.previous`
- `next` -> `page.next`
- Custom fields have moved from `<field>` to `page.data.<field>`.

BREAKING CHANGE: Liquid `{{ variables }}` have been renamed.

`cobalt migrate` will fix some.  `cobalt build` will most likely fail on
variables missed by `cobalt migrate`.
  • Loading branch information
epage committed Jan 4, 2018
1 parent 2331c06 commit 6f62dea
Show file tree
Hide file tree
Showing 136 changed files with 404 additions and 267 deletions.
3 changes: 3 additions & 0 deletions src/bin/cobalt/main.rs
Expand Up @@ -64,6 +64,9 @@ extern crate error_chain;
#[macro_use]
extern crate clap;

#[macro_use]
extern crate lazy_static;

#[macro_use]
extern crate log;

Expand Down
155 changes: 153 additions & 2 deletions src/bin/cobalt/migrate.rs
Expand Up @@ -24,7 +24,7 @@ pub fn migrate_command(matches: &clap::ArgMatches) -> Result<()> {
let config = args::get_config(matches)?;
let config = config.build()?;

migrate_includes(&config)?;
migrate_content(&config)?;
migrate_front(&config)?;

Ok(())
Expand Down Expand Up @@ -72,7 +72,64 @@ fn migrate_includes_path(content: String) -> Result<String> {
Ok(content)
}

fn migrate_includes(config: &cobalt::Config) -> Result<()> {
fn migrate_variables(content: String) -> Result<String> {
lazy_static!{
static ref REPLACEMENTS_REF: Vec<(regex::Regex, &'static str)> = vec![
// From tests
(r#"\{\{\s*title\s*}}"#, "{{ page.title }}"),
(r#"\{\{\s*(.*?)\.path\s*}}"#, "{{ $1.permalink }}"),
(r#"\{\{\s*path\s*}}"#, "{{ page.permalink }}"),
(r#"\{\{\s*content\s*}}"#, "{{ page.content }}"),
(r#"\{\{\s*previous\.(.*?)\s*}}"#, "{{ page.previous.$1 }}"),
(r#"\{\{\s*next\.(.*?)\s*}}"#, "{{ page.next.$1 }}"),
(r#"\{%\s*if\s+is_post\s*%}"#, "{% if page.is_post %}"),
(r#"\{%\s*if\s+previous\s*%}"#, "{% if page.previous %}"),
(r#"\{%\s*if\s+next\s*%}"#, "{% if page.next %}"),
(r#"\{%\s*if\s+draft\s*%}"#, "{% if page.is_draft %}"),
// from johannhof.github.io
(r#"\{\{\s*route\s*}}"#, "{{ page.data.route }}"),
(r#"\{%\s*if\s+route\s*"#, "{% if page.data.route "),
(r#"\{\{\s*date\s*"#, "{{ page.published_date "),
// From blog
(r#"\{\{\s*post\.date\s*"#, "{{ post.published_date "),
// From booyaa.github.io
(r#"\{%\s*assign\s+word_count\s*=\s*content"#, "{% assign word_count = page.content"),
(r#"\{%\s*assign\s+year\s*=\s*post.path"#, "{% assign year = post.permalink"),
(r#"\{%\s*assign\s+tags_list\s*=\s*post.tags"#, "{% assign tags_list = post.data.tags"),
(r#"\{%\s*assign\s+tags\s*=\s*post.tags"#, "{% assign tags = post.data.tags"),
// From deep-blog
(r#"\{%\s*if\s+lang\s*%}"#, "{% if page.data.lang %}"),
(r#"\{\{\s*lang\s*}}"#, "{{ page.data.lang }}"),
(r#"\{%\s*if\s+comments\s*%}"#, "{% if page.data.comments %}"),
(r#"\{%\s*if\s+dsq_thread_id\s*%}"#, "{% if page.data.dsq_thread_id %}"),
(r#"\{\{\s*dsq_thread_id\s*}}"#, "{{ page.data.dsq_thread_id }}"),
(r#"\{%\s*if\s+img_cover\s*%}"#, "{% if page.data.img_cover %}"),
(r#"\{\{\s*img_cover\s*}}"#, "{{ page.data.img_cover }}"),
(r#"\{%\s*if\s+post\.img_cover\s*%}"#, "{% if post.data.img_cover %}"),
(r#"\{\{\s*post\.img_cover\s*}}"#, "{{ post.data.img_cover }}"),
(r#"\{\{\s*post\.author\s*}}"#, "{{ post.data.author }}"),
// fnordig.de
(r#"\{%\s*assign\s+postyear\s*=\s*post.date"#,
"{% assign postyear = post.published_date"),
// hellorust
(r#"\{\{\s*author\s*}}"#, "{{ page.data.author }}"),
// mre
(r#"\{\{\s*title"#, "{{ page.title"),
(r#"\{%\s*if\s+title\s*!=\s*""\s*%}"#, r#"{% if page.title != "" %}"#),
(r#"\{%-\s*if\s+title\s*!=\s*""\s*%}"#, r#"{%- if page.title != "" %}"#),
].into_iter()
.map(|(r, s)| (regex::Regex::new(r).unwrap(), s))
.collect();
}
let content = REPLACEMENTS_REF
.iter()
.fold(content, |content, &(ref search, ref replace)| {
search.replace_all(&content, *replace).into_owned()
});
Ok(content)
}

fn migrate_content(config: &cobalt::Config) -> Result<()> {
let layouts_dir = config.source.join(config.layouts_dir);
let includes_dir = config.source.join(config.includes_dir);
info!("Migrating (potential) snippets to {:?}", includes_dir);
Expand Down Expand Up @@ -102,6 +159,7 @@ fn migrate_includes(config: &cobalt::Config) -> Result<()> {
}) {
let content = cobalt_model::files::read_file(&file)?;
let content = migrate_includes_path(content)?;
let content = migrate_variables(content)?;
cobalt_model::files::write_document_file(content, file)?;
}

Expand Down Expand Up @@ -182,4 +240,97 @@ mod tests {
let actual = migrate_includes_path(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_empty() {
let fixture = r#""#.to_owned();
let expected = r#""#.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_title() {
let fixture = r#"<h1>{{ path }}</h1>"#.to_owned();
let expected = r#"<h1>{{ page.permalink }}</h1>"#.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_path() {
let fixture = r#"<h2>{{ title }}</h2>"#.to_owned();
let expected = r#"<h2>{{ page.title }}</h2>"#.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_content() {
let fixture = r#"<h2>{{ content }}</h2>"#.to_owned();
let expected = r#"<h2>{{ page.content }}</h2>"#.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_scoped() {
let fixture = r#"<a href="{{post.path}}">{{ post.title }}</a>"#.to_owned();
let expected = r#"<a href="{{ post.permalink }}">{{ post.title }}</a>"#.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_previous() {
let fixture = r#"<a ref="/{{previous.path}}">&laquo; {{previous.title}}</a>"#.to_owned();
let expected =
r#"<a ref="/{{ page.previous.permalink }}">&laquo; {{ page.previous.title }}</a>"#
.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_next() {
let fixture = r#"<a class="next" href="/{{next.path}}">&laquo; {{next.title}}</a>"#
.to_owned();
let expected =
r#"<a class="next" href="/{{ page.next.permalink }}">&laquo; {{ page.next.title }}</a>"#
.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_if_is_post() {
let fixture = r#"{% if is_post %}"#.to_owned();
let expected = r#"{% if page.is_post %}"#.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_if_previous() {
let fixture = r#"{% if previous %}"#.to_owned();
let expected = r#"{% if page.previous %}"#.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_if_next() {
let fixture = r#"{% if next %}"#.to_owned();
let expected = r#"{% if page.next %}"#.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn migrate_variables_if_is_draft() {
let fixture = r#"{% if draft %}"#.to_owned();
let expected = r#"{% if page.is_draft %}"#.to_owned();
let actual = migrate_variables(fixture).unwrap();
assert_eq!(expected, actual);
}
}
44 changes: 19 additions & 25 deletions src/cobalt.rs
Expand Up @@ -155,25 +155,23 @@ pub fn build(config: &Config) -> Result<()> {
files::write_document_file(content, dest.join(file_path))?;
}

let mut globals = post.attributes.clone();
// TODO(epage): Switch `posts` to `parent` which is an object see #323
globals.insert("posts".to_owned(),
liquid::Value::Array(simple_posts_data.clone()));
globals.insert("site".to_owned(),
liquid::Value::Object(config.site.attributes.clone()));
// Everything done with `globals` is terrible for performance. liquid#95 allows us to
// improve this.
let mut globals: liquid::Object =
vec![("site".to_owned(), liquid::Value::Object(config.site.attributes.clone())),
("posts".to_owned(), liquid::Value::Array(simple_posts_data.clone()))]
.into_iter()
.collect();
globals.insert("page".to_owned(),
liquid::Value::Object(post.attributes.clone()));
post.render_excerpt(&globals, &parser, &config.syntax_highlight.theme)
.chain_err(|| format!("Failed to render excerpt for {:?}", post.file_path))?;
post.render_content(&globals, &parser, &config.syntax_highlight.theme)
.chain_err(|| format!("Failed to render content for {:?}", post.file_path))?;

// Yes, this is terrible for performance but we need a new `attributes` to get an
// updated `excerpt`. liquid#95 allow us to improve this.
let mut globals = post.attributes.clone();
// TODO(epage): Switch `posts` to `parent` which is an object see #323
globals.insert("posts".to_owned(),
liquid::Value::Array(simple_posts_data.clone()));
globals.insert("site".to_owned(),
liquid::Value::Object(config.site.attributes.clone()));
// Refresh `page` with the `excerpt` / `content` attribute
globals.insert("page".to_owned(),
liquid::Value::Object(post.attributes.clone()));
let post_html = post.render(&globals, &parser, &layouts, &mut layouts_cache)
.chain_err(|| format!("Failed to render for {:?}", post.file_path))?;
files::write_document_file(post_html, dest.join(&post.file_path))?;
Expand Down Expand Up @@ -214,20 +212,16 @@ pub fn build(config: &Config) -> Result<()> {
files::write_document_file(content, dest.join(file_path))?;
}

let mut globals = doc.attributes.clone();
// TODO(epage): Switch `posts` to `parent` which is an object see #323
globals.insert("posts".to_owned(), liquid::Value::Array(posts_data.clone()));
globals.insert("site".to_owned(),
liquid::Value::Object(config.site.attributes.clone()));
let mut globals: liquid::Object =
vec![("site".to_owned(), liquid::Value::Object(config.site.attributes.clone())),
("posts".to_owned(), liquid::Value::Array(posts_data.clone()))]
.into_iter()
.collect();
globals.insert("page".to_owned(),
liquid::Value::Object(doc.attributes.clone()));
doc.render_content(&globals, &parser, &config.syntax_highlight.theme)
.chain_err(|| format!("Failed to render content for {:?}", doc.file_path))?;

let mut globals = doc.attributes.clone();
// TODO(epage): Switch `posts` to `parent` which is an object see #323
globals.insert("posts".to_owned(), liquid::Value::Array(posts_data.clone()));
globals.insert("site".to_owned(),
liquid::Value::Object(config.site.attributes.clone()));

// Refresh `page` with the `excerpt` / `content` attribute
globals.insert("page".to_owned(),
liquid::Value::Object(doc.attributes.clone()));
Expand Down
59 changes: 30 additions & 29 deletions src/document.rs
Expand Up @@ -130,39 +130,39 @@ fn format_url_as_file_str(permalink: &str) -> PathBuf {
}

fn document_attributes(front: &cobalt_model::Frontmatter,
source_file: &str,
source_file: &Path,
url_path: &str)
-> liquid::Object {
let mut attributes = liquid::Object::new();
let categories = liquid::Value::Array(front
.categories
.iter()
.map(|c| liquid::Value::str(c))
.collect());
// Reason for `file`:
// - Allow access to assets in the original location
// - Ease linking back to page's source
let file: liquid::Object = vec![("permalink".to_owned(),
liquid::Value::str(source_file.to_str().unwrap_or("")))]
.into_iter()
.collect();
let attributes =
vec![("permalink".to_owned(), liquid::Value::str(url_path)),
("title".to_owned(), liquid::Value::str(&front.title)),
("description".to_owned(),
liquid::Value::str(front.description.as_ref().map(|s| s.as_str()).unwrap_or(""))),
("categories".to_owned(), categories),
("is_draft".to_owned(), liquid::Value::Bool(front.is_draft)),
("file".to_owned(), liquid::Value::Object(file)),
("data".to_owned(), liquid::Value::Object(front.data.clone()))];
let mut attributes: liquid::Object = attributes.into_iter().collect();

attributes.insert("path".to_owned(), liquid::Value::str(url_path));
// TODO(epage): Remove? See #257
attributes.insert("source".to_owned(), liquid::Value::str(source_file));
attributes.insert("title".to_owned(), liquid::Value::str(&front.title));
if let Some(ref description) = front.description {
attributes.insert("description".to_owned(), liquid::Value::str(description));
}
attributes.insert("categories".to_owned(),
liquid::Value::Array(front
.categories
.iter()
.map(|c| liquid::Value::str(c))
.collect()));
if let Some(ref published_date) = front.published_date {
// TODO(epage): Rename to published_date. See #257
attributes.insert("date".to_owned(),
attributes.insert("published_date".to_owned(),
liquid::Value::Str(published_date.format()));
}
// TODO(epage): Rename to `is_draft`. See #257
attributes.insert("draft".to_owned(), liquid::Value::Bool(front.is_draft));
// TODO(epage): Remove? See #257
// TODO(epage): Provide a way to determine tbis
attributes.insert("is_post".to_owned(), liquid::Value::Bool(front.is_post));

// TODO(epage): Place in a `custom` variable. See #257
for (key, val) in &front.data {
attributes.insert(key.clone(), val.clone());
}

attributes
}

Expand Down Expand Up @@ -213,8 +213,7 @@ impl Document {
(file_path, url_path)
};

let doc_attributes =
document_attributes(&front, rel_path.to_str().unwrap_or(""), url_path.as_ref());
let doc_attributes = document_attributes(&front, rel_path, url_path.as_ref());

Ok(Document::new(url_path,
file_path,
Expand Down Expand Up @@ -371,8 +370,10 @@ impl Document {
Ok(content_html)
} else {
let content_html = globals
.get("content")
.ok_or("Internal error: content isn't in globals")?
.get("page")
.ok_or("Internal error: page isn't in globals")?
.get(&liquid::Index::with_key("content"))
.ok_or("Internal error: page.content isn't in globals")?
.as_str()
.ok_or("Internal error: bad content format")?
.to_owned();
Expand Down
24 changes: 4 additions & 20 deletions src/new.rs
Expand Up @@ -21,32 +21,17 @@ const DEFAULT_LAYOUT: &'static str = "<!DOCTYPE html>
<html>
<head>
<meta charset=\"utf-8\">
{% if is_post %}
<title>{{ title }}</title>
{% else %}
<title>Cobalt.rs Blog</title>
{% endif %}
<title>{{ page.title }}</title>
</head>
<body>
<div>
{% if is_post %}
{% include '_layouts/post.liquid' %}
{% else %}
{{ content }}
{% endif %}
<h2>{{ page.title }}</h2>
{{ page.content }}
</div>
</body>
</html>
";

const POST_LAYOUT: &'static str = "<div>
<h2>{{ title }}</h2>
<p>
{{content}}
</p>
</div>
";

const POST_MD: &'static str = "layout: default.liquid
title: First Post
Expand All @@ -65,7 +50,7 @@ const INDEX_MD: &'static str = "layout: default.liquid
{% for post in posts %}
#### {{post.title}}
#### [{{ post.title }}]({{ post.path }})
[{{ post.title }}]({{ post.permalink }})
{% endfor %}
";

Expand All @@ -81,7 +66,6 @@ pub fn create_new_project_for_path(dest: &path::Path) -> Result<()> {

fs::create_dir_all(&dest.join("_layouts"))?;
create_file(&dest.join("_layouts/default.liquid"), DEFAULT_LAYOUT)?;
create_file(&dest.join("_layouts/post.liquid"), POST_LAYOUT)?;

fs::create_dir_all(&dest.join("posts"))?;
create_file(&dest.join("posts/post-1.md"), POST_MD)?;
Expand Down

0 comments on commit 6f62dea

Please sign in to comment.