Skip to content

Commit

Permalink
Update the readme
Browse files Browse the repository at this point in the history
  • Loading branch information
danott committed Jan 15, 2024
1 parent 5df90b7 commit 70298e9
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 28 deletions.
35 changes: 11 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This is the source code that generates my website.

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
3. `Target#hydrate` processes magic elements with all source data to fully compose the target
4. `Target#write` writes the target to `_site`

## Attaching data
Expand All @@ -24,41 +24,28 @@ Right now I use a bespoke data string format. I could imagine a future where I p

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

## Composing with "Magic Comments"
## Composing with "Magic Elements"

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.
I've implmented a minimal set of magic elements. 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.
Magic elements are deleted/replaced in the final output.

- `<!--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 place of the comment.
- `<include-in-header>children</include-in-header>` injects `children` into the document head.
- `<eval-ruby>"Any valid ruby code"</eval-ruby>` will evaluate the ruby. Eval is terrible and you should avoid it in production systems. I'm building my website so YOLO.

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

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 -->` works by accident. It's not necessary though...just type `something` directly!
- `<include-before-body>` 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 content at the beginning of the body.
- `<include-after-body>` for the same reasons.

### Ideas for the future

I have an idea that could be useful. But I haven't needed it yet.
I have an idea that could be useful. But I haven't needed them yet.

- `<!--Mutate::RegisteredMutation-->` for upgrading a `<h1>` to a `<header>` with sibling nodes, adding classnames to `<body>`, etc.
- `<eval-ruby>` or a new custom element could be introduced 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.
The beauty of magic elements is that they can be fractal. `<eval-ruby>` 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.
90 changes: 90 additions & 0 deletions lib/magic_element_hydrator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
class MagicElementHydrator
attr_reader :content
attr_reader :data

def initialize(content:, data:)
@content = content
@data = data
end

def hydrate
next_magic_element.present? ? self.next.hydrate : self
end

def next_magic_element
IncludeInHeader.recognize(content) || EvalRuby.recognize(content, self)
end

def next
fail "No more hydrating to do!" if next_magic_element.nil?
return self.class.new(content: next_magic_element.result, data: data)
end

private

class IncludeInHeader
attr_reader :content
attr_reader :match

def self.recognize(content)
match = content.match(%r{<include-in-header>(.*?)</include-in-header>}m)
new(content:, match:) if match
end

def initialize(content:, match:)
@content = content
@match = match
end

def end_of_head
content.index("</head>")
end

def entire_magic_element
match[0]
end

def children
match[1]
end

def result
content.sub(entire_magic_element, "").insert(end_of_head, children)
end
end

class EvalRuby
attr_reader :content
attr_reader :hydrator
attr_reader :match

def self.recognize(content, hydrator)
match = content.match(%r{<eval-ruby>(.*?)</eval-ruby>}m)
new(content:, match:, hydrator:) if match
end

def initialize(content:, match:, hydrator:)
@content = content
@match = match
@hydrator = hydrator
end

def entire_magic_element
match[0]
end

def ruby_code
match[1]
end

# Eval in the context of hydrator to have access to data
def replacement
hydrator.instance_eval(ruby_code).to_s
end

def result
position = content.index(entire_magic_element)
content.sub(entire_magic_element, "").insert(position, replacement)
end
end
end
10 changes: 6 additions & 4 deletions site/javascripts/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

console.log("Hello World!");

class IncludeHere extends HTMLElement {
class EvalRuby extends HTMLElement {
connectedCallback() {
console.error("Unexpected Custom element added to page!");
console.error(
"Unexpected element added to page! This should have been removed when building the site!"
);
console.error(this);
this.parentElement.removeChild(this);
}
}

class IncludeInHeader extends IncludeHere {}
class IncludeInHeader extends EvalRuby {}

customElements.define("eval-ruby", EvalRuby);
customElements.define("include-in-header", IncludeInHeader);
customElements.define("eval-ruby", IncludeHere);
55 changes: 55 additions & 0 deletions test/magic_element_hydrator_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
class MagicElementHydratorTest < Minitest::Test
CONTENT = <<~HTML
<html>
<head>
<title>Test</title>
</head>
<body>
<h1>Test</h1>
<h2><eval-ruby>2 + 2</eval-ruby></h2>
<h2><eval-ruby>self.data.fetch("Fetch this!")</eval-ruby></h2>
<include-in-header>
<meta name="include-in-header">
<eval-ruby>
%Q[<meta name="\#{data.fetch("Fetch this!")}">]
</eval-ruby>
</include-in-header>
</body>
</html>
HTML

def test_everything
hydrator =
MagicElementHydrator.new(
content: CONTENT,
data: {
"Fetch this!" => "Return that!"
}
)
result = hydrator.hydrate

refute_includes result.content, "Fetch this!"
refute_includes result.content, "2 + 2"
refute_includes result.content, "<eval-ruby>"
refute_includes result.content, "</eval-ruby>"
refute_includes result.content, "<include-in-header>"
refute_includes result.content, "</include-in-header>"

assert_body_includes result.content, "<h2>4</h2>"
assert_body_includes result.content, "<h2>Return that!</h2>"

assert_head_includes result.content, '<meta name="include-in-header">'
assert_head_includes result.content, '<meta name="Return that!">'
end

def assert_body_includes(haystack, needle)
body = haystack.split("<body>").last.split("</body>").first
assert_includes body, needle
end

def assert_head_includes(haystack, needle)
head = haystack.split("<head>").last.split("</head>").first
assert_includes head, needle
end
end

0 comments on commit 70298e9

Please sign in to comment.