Skip to content

Commit

Permalink
Separate the concepts of #hydrate and #to_target
Browse files Browse the repository at this point in the history
This multi-pass build should have distinct concepts. Hydration should
mean one thing, and now it does.
  • Loading branch information
danott committed Jan 15, 2024
1 parent 6c74698 commit e252e1c
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 37 deletions.
46 changes: 34 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,54 @@ This is the source code that generates my website.

`Build` is a singleton. I've commited some inadvisable meta-programming so I can call `Build.sources` instead of `Build.instance.sources`.

1. `Source.gather` gathers sources from disk in `site`
2. `Source#hydrate` transforms the source into a target with data attached
3. `Target#hydrate` processes magic comments to fully compose the target
4. `Target#write` writes the target to disk in `_site`
1. `Source.gather` gathers sources from `site`
2. `Source#to_target` transforms the sources into targets with data attached
3. `Target#hydrate` processes magic comments with all source data to fully compose the target
4. `Target#write` writes the target to `_site`

## Magic comments
## Attaching data

I use an inert HTML element to associate data with a source. It looks like`<template data-parse>[data string]</template>`.

Right now I use a bespoke data string format. I could imagine a future where I process other languages like `<template data-parse="(bespoke|json|yaml|toml)">`. But I'm avoiding YAGNI.

This parsing should happen before a source is turned into a target.

## Composing with "Magic Comments"

I've implmented a minimal set of magic comments. I'm leaning into the idea that composition is superior to inheritance. So I'm not *inheriting* from multiple "templates" or "layouts". I'm *composing* individual pages by injecting and/or replacing content.

Magic comments are deleted/replaced in the final output.

- `<template data-parse></template>` is processed during `Source#hydrate`. It has to be processed early because every call to `Target#hydrate` wants access to all the site data across all sources/targets.
- `<!--IncludeInHeader::Text:: [any text value] -->` injects `[any text value]` into the document head.
- `<!--IncludeInHeader::Text [string] -->` injects `[string]` into the document head.
- `<!--IncludeInHeader::RegisteredIncludable-->` will look up `RegisteredIncludable`, render it, and inject the result into the document head.
- `<!--Include::RegisteredIncludable-->` will look up `RegisteredIncludable`, render it, and inject the result in palce of the comment.
- `<!--Include::RegisteredIncludable-->` will look up `RegisteredIncludable`, render it, and inject the result in place of the comment.

### Not implemented ideas because they don't need to exist

- `<!--IncludeBeforeBody::RegisteredIncludable-->` because I don't have layers of nested layouts. If I want something at the beginning of the body I include the magic comment at the beginning of the body.
I really like `pandoc`. It has three options that inspire me. `--include-in-header`, `--include-before-body`, and `--include-after-body`. I've only implemented one.

- `<!--IncludeBeforeBody::RegisteredIncludable-->` isn't necessary because I don't have layers of nested layouts. If I want something at the beginning of the body I include the magic comment at the beginning of the body.
- `<!--IncludeAfterBody::RegisteredIncludable-->` for the same reasons.
- `<!--Include::Text:: something -->` because that's silly. Just write `something` and it's included, right there.
- `<!--Include::Text something -->` works by accident. It's not necessary though...just type `something` directly!

### Not implemented but maybe there's a use case but I'm resisting YAGNI
### Ideas for the future

- `<!--Mutate::RegisteredMutation-->` for upgrading a `<h1>`, adding classnames to `<body>`, etc.
I have an idea that could be useful. But I haven't needed it yet.

- `<!--Mutate::RegisteredMutation-->` for upgrading a `<h1>` to a `<header>` with sibling nodes, adding classnames to `<body>`, etc.

The beauty of magic comments is that they can be fractal. `<!--Include::RegisteredIncludable-->` can be a hydra that generates other magic comments that will be recursively processed.

There's potential for infinite loops here. But who cares. I'm building my website. Not a generic tool for building any website.

I do wonder if it'd be worth using something besides a comment for this. Something like custom elements?

```html
<include-in-header includable="Text">Some text value</include-in-header>
<include-here includable="RegisteredIncludable"></include-here>
```

I could even implement these custom elements in JavaScript. If they show up it could be noisy and throw errors. I could make an XHR to a bespoke netlify endpoint to notify myself of the error. I could write tests that fail if anything in `_site` includes these custom elements.

Maybe someday.
6 changes: 3 additions & 3 deletions lib/build.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ def data_dir
"data"
end

def dehydrated_sources
@dehydrated_sources ||= Source.gather(source_dir)
def sources
@sources ||= Source.gather(source_dir)
end

def dehydrated_targets
@dehydrated_targets ||= dehydrated_sources.map(&:hydrate)
@dehydrated_targets ||= sources.map(&:to_target)
end

def hydrated_targets
Expand Down
4 changes: 4 additions & 0 deletions lib/feed_target.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ class FeedTarget
URL = "https://danott.website".freeze
FIRST_DANOTT_WEBSITE_POST_DATE = Date.parse("2024-01-13")

def to_target
self
end

def hydrate
self
end
Expand Down
10 changes: 7 additions & 3 deletions lib/file_copier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ def initialize(path)
@path = path
end

def write
FileUtils.mkdir_p(File.dirname(target_path))
FileUtils.cp(path, target_path)
def to_target
self
end

def hydrate
self
end

def write
FileUtils.mkdir_p(File.dirname(target_path))
FileUtils.cp(path, target_path)
end

def target_path
path.sub(Build.source_dir, Build.target_dir)
end
Expand Down
24 changes: 12 additions & 12 deletions lib/markdown_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ def initialize(content:, path:)
@path = path
end

def hydrate
def to_target
next_content =
Kramdown::Document.new(content, kramdown_options).to_html.strip
data = {}
next_content, data = hydrate_data_comment(next_content, data)
next_content, data = hydrate_title_data(next_content, data)
next_content, data = hydrate_html_template_data(next_content)
data = data.merge(infer_title_data(next_content))
HtmlTarget.new(path: path, content: next_content, data: data)
end

Expand All @@ -29,25 +28,26 @@ def kramdown_options
}
end

def hydrate_data_comment(content, data)
def hydrate_html_template_data(content)
data_regex = %r{<template data-parse.*?>(.*?)</template>}m

if match = content.match(data_regex)
data.merge! parse_plaintext_data_line(match[1])
data = parse_plaintext_data_line(match[1])
content = content.sub(match[0], "")
[content, data]
else
[content, {}]
end

[content, data]
end

def hydrate_title_data(content, data)
def infer_title_data(content)
title_regex = %r{<title>(.*?)</title>}m

if match = content.match(title_regex)
data.merge!("title" => match[1].strip)
{ "title" => match[1].strip }
else
{}
end

[content, data]
end

def parse_plaintext_data_line(line)
Expand Down
8 changes: 6 additions & 2 deletions lib/noop_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ def initialize(path)
@path = path
end

def write
puts "NoopSource#write: #{path}"
def to_target
self
end

def hydrate
self
end

def write
puts "NoopSource#write: #{path}"
self
end
end
10 changes: 5 additions & 5 deletions test/markdown_source_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ def test_source_hydrate
assert_equal PLAINTEXT, original_source.content
assert_equal "site/dummy.md", original_source.path

hydrated_source = original_source.hydrate
html_target = original_source.to_target

# Avoid mutation of the original source. Generate new instances instead
assert_equal PLAINTEXT, original_source.content
assert_equal "site/dummy.md", original_source.path

# Test hydrated source
refute_equal PLAINTEXT, hydrated_source.content, "becomes html"
refute_includes hydrated_source.content,
refute_equal PLAINTEXT, html_target.content, "becomes html"
refute_includes html_target.content,
"<template data-parse",
"data comment has been hydrated into Source#data"
assert_equal "site/dummy.md", hydrated_source.path, "unchanged in hydration"
assert_equal "site/dummy.md", html_target.path, "unchanged in hydration"

expected_data = {
"date" => Date.parse("2024-01-01"),
Expand All @@ -38,7 +38,7 @@ def test_source_hydrate

expected_data.each do |key, value|
assert_equal value,
hydrated_source.data[key],
html_target.data[key],
"data in #{key} does not match!"
end
end
Expand Down

0 comments on commit e252e1c

Please sign in to comment.