@whittakertech/virgil 0.1.0 → 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.1] - 2025-12-30
11
+ ### Added
12
+ - Allow optional image blocks in templates via presence-based guards (`if / endif`)
13
+ - Reconfigure template to conditionally render brand and product logos
14
+
15
+ ## [0.1.0] - 2025-12-29
16
+ ### Added
17
+ - Initial Virgil specification for build-time artifact generation
18
+ - Support for Open Graph image generation via headless Chromium (Playwright)
19
+ - Deterministic OG image rendering from HTML/CSS templates
20
+ - Cache-busted asset filenames for safe static deployment
21
+ - `virgil.manifest.json` output mapping stable IDs to generated artifacts
22
+ - Brand and product configuration support (name, color, version)
23
+ - Page metadata support for OG images (title, URL, description)
24
+
25
+ ### Notes
26
+ - This release establishes Virgil’s core philosophy: deterministic, build-time artifact generation with no runtime dependencies
27
+ - Template rendering supports static variable substitution only (no logic or control flow)
@@ -1 +1 @@
1
- {"version":3,"file":"og-image.d.ts","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAK7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,cAAc,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAkCf;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG1D"}
1
+ {"version":3,"file":"og-image.d.ts","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAK7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAWD;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,cAAc,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG1D"}
@@ -4,6 +4,15 @@ import { join, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { ensureParentDir } from '../utils/fs.js';
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ function getPath(obj, path) {
8
+ return path
9
+ .split('.')
10
+ .reduce((acc, key) => {
11
+ if (acc == null || typeof acc !== 'object')
12
+ return undefined;
13
+ return acc[key];
14
+ }, obj);
15
+ }
7
16
  /**
8
17
  * Generate OG image using Playwright
9
18
  */
@@ -11,11 +20,25 @@ export async function generateOGImage(ctx, outputPath) {
11
20
  // Load template
12
21
  const templatePath = ctx.templatePath || join(__dirname, '../templates/og-default.html');
13
22
  let html = await readFile(templatePath, 'utf-8');
23
+ const renderCtx = {
24
+ brand: ctx.brand,
25
+ product: ctx.product,
26
+ page: ctx.output.page,
27
+ output: ctx.output
28
+ };
29
+ // Omit falsy blocks
30
+ const IF_BLOCK = /{{\s*if\s+([a-zA-Z0-9_.]+)\s*}}([\s\S]*?){{\s*endif\s*}}/g;
31
+ html = html.replace(IF_BLOCK, (_, condition, body) => {
32
+ const value = getPath(renderCtx, condition);
33
+ return value !== undefined && value !== null ? body : '';
34
+ });
14
35
  // Replace placeholders
15
36
  html = html
16
37
  .replace(/{{brand.name}}/g, ctx.brand.name)
38
+ .replace(/{{brand.logo}}/g, ctx.brand.logo || '')
17
39
  .replace(/{{brand.color}}/g, ctx.brand.color)
18
40
  .replace(/{{product.name}}/g, ctx.product.name)
41
+ .replace(/{{product.logo}}/g, ctx.product.logo || '')
19
42
  .replace(/{{product.version}}/g, ctx.product.version)
20
43
  .replace(/{{page.title}}/g, ctx.output.page.title)
21
44
  .replace(/{{page.url}}/g, ctx.output.page.url)
@@ -1 +1 @@
1
- {"version":3,"file":"og-image.js","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAa,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAS1D;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAmB,EACnB,UAAkB;IAElB,gBAAgB;IAChB,MAAM,YAAY,GAAG,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,SAAS,EAAE,8BAA8B,CAAC,CAAC;IACzF,IAAI,IAAI,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAEjD,uBAAuB;IACvB,IAAI,GAAG,IAAI;SACR,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;SAC1C,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC;SAC5C,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC;SAC9C,OAAO,CAAC,sBAAsB,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC;SACpD,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;SACjD,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;SAC7C,OAAO,CAAC,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAEvE,0BAA0B;IAC1B,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC;QACjC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;KACvC,CAAC,CAAC;IAEH,wCAAwC;IACxC,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC;IAE1D,iCAAiC;IACjC,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IAElC,aAAa;IACb,MAAM,IAAI,CAAC,UAAU,CAAC;QACpB,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,KAAK;KACZ,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;AACxB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,EAAU;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAChD,OAAO,GAAG,EAAE,IAAI,SAAS,MAAM,CAAC;AAClC,CAAC"}
1
+ {"version":3,"file":"og-image.js","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"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"}
@@ -83,7 +83,13 @@
83
83
  <body>
84
84
  <div class="container">
85
85
  <div class="header">
86
- <div class="brand">{{brand.name}}</div>
86
+ {{ if brand.logo }}
87
+ <div class="image">
88
+ <img src="{{brand.logo}}" alt="{{brand.name}} logo">
89
+ </div>
90
+ {{ endif }}
91
+ <div class="product">{{product.name}}</div>
92
+ <div class="version">v{{product.version}}</div>
87
93
  </div>
88
94
 
89
95
  <div class="content">
@@ -92,8 +98,12 @@
92
98
  </div>
93
99
 
94
100
  <div class="footer">
95
- <div class="product">{{product.name}}</div>
96
- <div class="version">v{{product.version}}</div>
101
+ {{ if brand.logo }}
102
+ <div class="image">
103
+ <img src="{{brand.logo}}" alt="{{brand.name}} logo">
104
+ </div>
105
+ {{ endif }}
106
+ <div class="brand">{{brand.name}}</div>
97
107
  </div>
98
108
  </div>
99
109
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whittakertech/virgil",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Build-phase sigil forge for static site artifacts",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,6 +14,15 @@ export interface OGImageContext {
14
14
  templatePath?: string;
15
15
  }
16
16
 
17
+ function getPath(obj: unknown, path: string): unknown {
18
+ return path
19
+ .split('.')
20
+ .reduce((acc: any, key) => {
21
+ if (acc == null || typeof acc !== 'object') return undefined;
22
+ return acc[key];
23
+ }, obj);
24
+ }
25
+
17
26
  /**
18
27
  * Generate OG image using Playwright
19
28
  */
@@ -25,15 +34,31 @@ export async function generateOGImage(
25
34
  const templatePath = ctx.templatePath || join(__dirname, '../templates/og-default.html');
26
35
  let html = await readFile(templatePath, 'utf-8');
27
36
 
37
+ const renderCtx = {
38
+ brand: ctx.brand,
39
+ product: ctx.product,
40
+ page: ctx.output.page,
41
+ output: ctx.output
42
+ }
43
+ // Omit falsy blocks
44
+ const IF_BLOCK =
45
+ /{{\s*if\s+([a-zA-Z0-9_.]+)\s*}}([\s\S]*?){{\s*endif\s*}}/g;
46
+ html = html.replace(IF_BLOCK, (_, condition, body) => {
47
+ const value = getPath(renderCtx, condition);
48
+ return value !== undefined && value !== null ? body : '';
49
+ });
50
+
28
51
  // Replace placeholders
29
52
  html = html
30
- .replace(/{{brand.name}}/g, ctx.brand.name)
31
- .replace(/{{brand.color}}/g, ctx.brand.color)
32
- .replace(/{{product.name}}/g, ctx.product.name)
33
- .replace(/{{product.version}}/g, ctx.product.version)
34
- .replace(/{{page.title}}/g, ctx.output.page.title)
35
- .replace(/{{page.url}}/g, ctx.output.page.url)
36
- .replace(/{{page.description}}/g, ctx.output.page.description || '');
53
+ .replace(/{{brand.name}}/g, ctx.brand.name)
54
+ .replace(/{{brand.logo}}/g, ctx.brand.logo || '')
55
+ .replace(/{{brand.color}}/g, ctx.brand.color)
56
+ .replace(/{{product.name}}/g, ctx.product.name)
57
+ .replace(/{{product.logo}}/g, ctx.product.logo || '')
58
+ .replace(/{{product.version}}/g, ctx.product.version)
59
+ .replace(/{{page.title}}/g, ctx.output.page.title)
60
+ .replace(/{{page.url}}/g, ctx.output.page.url)
61
+ .replace(/{{page.description}}/g, ctx.output.page.description || '');
37
62
 
38
63
  // Launch headless browser
39
64
  const browser = await chromium.launch();
@@ -83,7 +83,13 @@
83
83
  <body>
84
84
  <div class="container">
85
85
  <div class="header">
86
- <div class="brand">{{brand.name}}</div>
86
+ {{ if brand.logo }}
87
+ <div class="image">
88
+ <img src="{{brand.logo}}" alt="{{brand.name}} logo">
89
+ </div>
90
+ {{ endif }}
91
+ <div class="product">{{product.name}}</div>
92
+ <div class="version">v{{product.version}}</div>
87
93
  </div>
88
94
 
89
95
  <div class="content">
@@ -92,8 +98,12 @@
92
98
  </div>
93
99
 
94
100
  <div class="footer">
95
- <div class="product">{{product.name}}</div>
96
- <div class="version">v{{product.version}}</div>
101
+ {{ if brand.logo }}
102
+ <div class="image">
103
+ <img src="{{brand.logo}}" alt="{{brand.name}} logo">
104
+ </div>
105
+ {{ endif }}
106
+ <div class="brand">{{brand.name}}</div>
97
107
  </div>
98
108
  </div>
99
109
  </body>