@jsnchn/buntastic 0.0.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -64,6 +64,7 @@ draft: false # Set true to exclude from production build
64
64
  | `{{ description }}` | Page description |
65
65
  | `{{ url }}` | Current page URL |
66
66
  | `{{ collection }}` | Array of posts in current folder (for index pages) |
67
+ | `{{ head \| safe }}` | Head content from child layouts (appended to parent) |
67
68
 
68
69
  ## Layout System
69
70
 
@@ -82,6 +83,28 @@ extends: base.html
82
83
 
83
84
  The `{{ content | safe }}` placeholder is where child content gets injected.
84
85
 
86
+ ## Head Block
87
+
88
+ Layouts can inject content into the `<head>` section using `[head]...[/head]` blocks:
89
+
90
+ ```html
91
+ <!-- layouts/post.html -->
92
+ ---
93
+ extends: base.html
94
+ ---
95
+ [head]
96
+ <link rel="stylesheet" href="/post.css">
97
+ [/head]
98
+ [content]
99
+ <article class="post">
100
+ <h1>{{ title }}</h1>
101
+ {{ content | safe }}
102
+ </article>
103
+ [/content]
104
+ ```
105
+
106
+ Content inside `[head]` blocks is automatically placed in the parent's `<head>` section. Child layout head content is appended to parent head content.
107
+
85
108
  ## Development
86
109
 
87
110
  1. Add content to `content/` folder
package/README.md CHANGED
@@ -164,11 +164,12 @@ extends: base.html
164
164
  | Variable | Description |
165
165
  |----------|-------------|
166
166
  | `{{ title }}` | Page title |
167
- | `{{ content \| safe }}` | Rendered markdown HTML |
167
+ | `{{ content | safe }}` | Rendered markdown HTML |
168
168
  | `{{ date }}` | Page date |
169
169
  | `{{ description }}` | Meta description |
170
170
  | `{{ url }}` | Current page URL |
171
171
  | `{{ collection }}` | List of posts in current folder (for index pages) |
172
+ | `{{ head | safe }}` | Head content from child layouts (appended to parent) |
172
173
 
173
174
  ### Layout Inheritance
174
175
 
@@ -187,6 +188,28 @@ Use `extends:` to build on top of other layouts:
187
188
  </html>
188
189
  ```
189
190
 
191
+ ### Head Block
192
+
193
+ Layouts can inject content into the `<head>` section using `[head]...[/head]` blocks:
194
+
195
+ ```html
196
+ <!-- layouts/post.html -->
197
+ ---
198
+ extends: base.html
199
+ ---
200
+ [head]
201
+ <link rel="stylesheet" href="/post.css">
202
+ [/head]
203
+ [content]
204
+ <article class="post">
205
+ <h1>{{ title }}</h1>
206
+ {{ content | safe }}
207
+ </article>
208
+ [/content]
209
+ ```
210
+
211
+ Content inside `[head]` blocks is automatically placed in the parent's `<head>` section. Child layout head content is appended to parent head content.
212
+
190
213
  ## Collections
191
214
 
192
215
  For folder index pages (e.g., `content/posts/index.md`), use `{{ collection }}` to list all pages in that folder:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsnchn/buntastic",
3
- "version": "0.0.12",
3
+ "version": "0.2.0",
4
4
  "description": "A simple static site generator built with Bun",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -94,21 +94,78 @@ async function readLayout(layoutName: string): Promise<string> {
94
94
  return await readFile(layoutPath, "utf-8");
95
95
  }
96
96
 
97
- async function resolveLayout(frontmatter: Frontmatter): Promise<string> {
97
+ async function resolveLayout(frontmatter: Frontmatter): Promise<{ template: string; head: string }> {
98
98
  const layoutName = frontmatter.layout || frontmatter.extends || "page";
99
99
  let template = await readLayout(layoutName);
100
100
 
101
101
  const extendsMatch = template.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
102
+
103
+ let childHead = "";
104
+ let childBody = "";
105
+
102
106
  if (extendsMatch) {
103
107
  const parentLayout = extendsMatch[1].match(/extends:\s*(\w+)/);
104
108
  if (parentLayout) {
105
- const parentTemplate = await resolveLayout({ extends: parentLayout[1] } as Frontmatter);
106
109
  const childContent = extendsMatch[2];
107
- return parentTemplate.replace(/\{\{\s*content\s*\|\s*safe\s*\}\}/g, childContent);
110
+
111
+ const headMatch = childContent.match(/\[head\]([\s\S]*?)\[\/head\]/);
112
+ if (headMatch) {
113
+ childHead = headMatch[1].trim();
114
+ }
115
+
116
+ const bodyMatch = childContent.match(/\[content\]([\s\S]*?)\[\/content\]/);
117
+ if (bodyMatch) {
118
+ childBody = bodyMatch[1];
119
+ } else {
120
+ childBody = childContent;
121
+ }
122
+
123
+ const parentResult = await resolveLayout({ extends: parentLayout[1] } as Frontmatter);
124
+
125
+ let contentReplaced = parentResult.template;
126
+
127
+ if (bodyMatch) {
128
+ contentReplaced = contentReplaced.replace(
129
+ /\[content\]([\s\S]*?)\[\/content\]/g,
130
+ childBody
131
+ );
132
+ } else {
133
+ contentReplaced = contentReplaced.replace(
134
+ /\{\{\s*content\s*\|\s*safe\s*\}\}/g,
135
+ childBody
136
+ );
137
+ }
138
+
139
+ const mergedHead = parentResult.head + (childHead ? "\n" + childHead : "");
140
+
141
+ if (headMatch) {
142
+ contentReplaced = contentReplaced.replace(
143
+ /\[head\]([\s\S]*?)\[\/head\]/g,
144
+ mergedHead
145
+ );
146
+ } else {
147
+ contentReplaced = contentReplaced.replace(
148
+ /\[head\]([\s\S]*?)\[\/head\]/g,
149
+ mergedHead
150
+ );
151
+ contentReplaced = contentReplaced.replace(
152
+ /\{\{\s*head\s*\|\s*safe\s*\}\}/g,
153
+ mergedHead
154
+ );
155
+ }
156
+
157
+ return { template: contentReplaced, head: mergedHead };
108
158
  }
109
159
  }
110
160
 
111
- return template;
161
+ let ownHead = "";
162
+ if (extendsMatch) {
163
+ const bodyMatch = extendsMatch[2];
164
+ const headMatch = bodyMatch.match(/\[head\]([\s\S]*?)\[\/head\]/);
165
+ ownHead = headMatch ? headMatch[1].trim() : "";
166
+ }
167
+
168
+ return { template, head: ownHead };
112
169
  }
113
170
 
114
171
  function applyTemplate(template: string, page: Page, collection?: CollectionItem[]): string {
@@ -213,7 +270,7 @@ async function build(includeDrafts = false): Promise<void> {
213
270
  }
214
271
 
215
272
  for (const page of pages) {
216
- let template = await resolveLayout(page.frontmatter);
273
+ let { template } = await resolveLayout(page.frontmatter);
217
274
  template = template.replace(/\{\{\s*content\s*\|\s*safe\s*\}\}/g, page.html);
218
275
 
219
276
  let collection: CollectionItem[] | undefined;
@@ -266,7 +323,7 @@ async function build(includeDrafts = false): Promise<void> {
266
323
  if (await exists(join(CONTENT_DIR, "404.md"))) {
267
324
  const page404 = await buildPage(join(CONTENT_DIR, "404.md"), includeDrafts);
268
325
  if (page404) {
269
- let template = await resolveLayout(page404.frontmatter);
326
+ let { template } = await resolveLayout(page404.frontmatter);
270
327
  template = template.replace(/\{\{\s*content\s*\|\s*safe\s*\}\}/g, page404.html);
271
328
  const outputHtml = applyTemplate(template, page404);
272
329
  await writeFile(join(DIST_DIR, "404.html"), outputHtml);
@@ -6,6 +6,8 @@
6
6
  <title>{{ title }}</title>
7
7
  <meta name="description" content="{{ description }}">
8
8
  <link rel="stylesheet" href="/style.css">
9
+ [head]
10
+ [/head]
9
11
  </head>
10
12
  <body>
11
13
  <header>
@@ -16,7 +18,8 @@
16
18
  </nav>
17
19
  </header>
18
20
  <main>
19
- {{ content | safe }}
21
+ [content]
22
+ [/content]
20
23
  </main>
21
24
  <footer>
22
25
  <p>Built with BunPress</p>
@@ -1,7 +1,9 @@
1
1
  ---
2
2
  extends: base.html
3
3
  ---
4
+ [content]
4
5
  <article class="page">
5
6
  <h1>{{ title }}</h1>
6
7
  {{ content | safe }}
7
8
  </article>
9
+ [/content]
@@ -1,6 +1,10 @@
1
1
  ---
2
2
  extends: base.html
3
3
  ---
4
+ [head]
5
+ <link rel="stylesheet" href="/post.css">
6
+ [/head]
7
+ [content]
4
8
  <article class="post">
5
9
  <header>
6
10
  <h1>{{ title }}</h1>
@@ -10,3 +14,4 @@ extends: base.html
10
14
  {{ content | safe }}
11
15
  </div>
12
16
  </article>
17
+ [/content]