AtlatestRepositorypress

press / tree / srcpress.sgl

1;;; (press) - Static Site Generation Library
2;;;
3;;; A composable pipeline for generating websites from various content
4;;; formats. Library-first design with functions and dicts.
5;;;
6;;; ## Quick Start
7;;;
8;;; ```scheme
9;;; (import (press))
10;;;
11;;; ;; Build a site
12;;; (let ((my-site (site title: "My Site"
13;;; content: "content/**/*.md")))
14;;; (execute-graph (build-graph (my-site config:) (my-site pipeline:))))
15;;; ```
16;;;
17;;; ## Architecture
18;;;
19;;; Two-phase execution for memory efficiency:
20;;; 1. Build Graph: Scan sources, create items with producers
21;;; 2. Execute: Call producers to render content
22;;;
23;;; For incremental rebuilds, use execute-changed instead of execute-graph.
25(define-library (press)
26 (import (sigil core)
27 (sigil string)
28 (sigil path)
29 (sigil fs)
30 (sigil io)
31 (sigil ansi)
32 (sigil process)
33 (sigil watch)
34 (press core)
35 (press scan)
36 (press stages)
37 (press render)
38 (press generate)
39 (press feeds)
40 (press transform)
41 (press dev-server)
42 (press docs))
43 (export
44 ;; High-level site builder
45 site
47 ;; Script runner
48 run-publish
50 ;; Pipeline execution (re-export from core)
51 build-graph
52 execute-graph
53 execute-changed
55 ;; Pipeline stages (re-export)
56 scan-files
57 scan-assets
58 filter-drafts
59 build-navigation
60 build-ref-index
61 build-path-index
62 attach-content-producers
63 attach-asset-producers
64 rewrite-links
65 resolve-refs
67 ;; Content generators (re-export from generate)
68 collection-page
69 sxml-page
71 ;; Feeds (re-export from feeds)
72 with-feeds
74 ;; Theme support (re-export)
75 default-theme
76 sxml->html
78 ;; Transforms (re-export)
79 syntax-highlight
81 ;; State accessors (re-export)
82 state-items
83 state-assets
84 state-navigation
85 state-ref-index
86 state-path-index
88 ;; Documentation publishing (re-export from docs)
89 docs-theme
90 generate-docs-css
91 scan-api-docs
92 scan-library-docs
93 lib-index-template
94 module-details-template
95 export-details->sxml
96 syntax-details->sxml
97 format-library-name
98 module-details->output-path)
100 (begin
102 ;; ============================================================
103 ;; High-Level Site Builder
104 ;; ============================================================
106 ;;; Create a site configuration.
107 ;;;
108 ;;; Returns a dict with:
109 ;;; - config: Configuration dict for the pipeline
110 ;;; - pipeline: List of pipeline stages
111 ;;; - watch-patterns: List of glob patterns to watch for changes
112 ;;;
113 ;;; The `pipeline:` argument can be:
114 ;;; - A list: Use as the complete pipeline (replaces default)
115 ;;; - A procedure: Called with default pipeline, returns modified pipeline
116 ;;; - Omitted: Use auto-generated pipeline from content/assets
117 ;;;
118 ;;; ```scheme
119 ;;; ;; Simple site with defaults
120 ;;; (site title: "My Blog"
121 ;;; content: "posts/**/*.md"
122 ;;; assets: "static/**/*")
123 ;;;
124 ;;; ;; Custom pipeline (complete override)
125 ;;; (site title: "My Site"
126 ;;; output-dir: "public"
127 ;;; pipeline: (list (scan-files "docs/**/*.md")
128 ;;; filter-drafts
129 ;;; build-navigation
130 ;;; attach-content-producers))
131 ;;;
132 ;;; ;; Modify default pipeline
133 ;;; (site title: "My Docs"
134 ;;; content: "docs/**/*.md"
135 ;;; pipeline: (lambda (stages)
136 ;;; (append stages (list generate-docs-css))))
137 ;;; ```
138 (define (site . args)
139 (let ((title (extract-keyword-arg args title: "Site"))
140 (base-url (extract-keyword-arg args base-url: "/"))
141 (content-patterns (extract-keyword-arg args content: '()))
142 (asset-pattern (extract-keyword-arg args assets: #f))
143 (theme (extract-keyword-arg args theme: default-theme))
144 (transforms (extract-keyword-arg args transforms: (list rewrite-links)))
145 (output-dir (extract-keyword-arg args output-dir: "out"))
146 (draft-mode (extract-keyword-arg args draft-mode: #f))
147 (custom-pipeline (extract-keyword-arg args pipeline: #f))
148 (custom-watch (extract-keyword-arg args watch-patterns: #f)))
149 (let* ((content-list (cond
150 ((null? content-patterns) '())
151 ((string? content-patterns) (list content-patterns))
152 (else content-patterns)))
153 ;; Default pipeline built from content/assets
154 (default-pipeline
155 (append
156 (map scan-files content-list)
157 (if asset-pattern
158 (list (scan-assets asset-pattern))
159 '())
160 (list filter-drafts
161 build-navigation
162 build-ref-index
163 build-path-index
164 attach-content-producers
165 attach-asset-producers)))
166 ;; Final pipeline: custom list, modified default, or default
167 (final-pipeline
168 (cond
169 ((list? custom-pipeline) custom-pipeline)
170 ((procedure? custom-pipeline) (custom-pipeline default-pipeline))
171 (else default-pipeline)))
172 ;; Watch patterns
173 (default-watch (append content-list
174 (if asset-pattern (list asset-pattern) '())))
175 (final-watch (or custom-watch default-watch)))
176 #{config: #{title: title
177 base-url: base-url
178 output-dir: output-dir
179 theme: theme
180 transforms: transforms
181 options: #{draft-mode: draft-mode}}
182 pipeline: final-pipeline
183 watch-patterns: final-watch})))
185 ;; ============================================================
186 ;; Script Runner
187 ;; ============================================================
189 ;;; Run a publish script with build/watch/serve commands.
190 ;;;
191 ;;; Parses command-line arguments and executes the appropriate action:
192 ;;; - build: One-shot site generation
193 ;;; - watch: Rebuild on file changes (no server)
194 ;;; - serve: HTTP server + watch + live reload
195 ;;;
196 ;;; Usage in scripts:
197 ;;; ```scheme
198 ;;; (import (press))
199 ;;;
200 ;;; (define my-site
201 ;;; (site title: "My Site" ...))
202 ;;;
203 ;;; (run-publish my-site)
204 ;;; ```
205 ;;;
206 ;;; Then run: `sigil site.sgl build` or `sigil site.sgl serve`
207 (define (run-publish site-spec)
208 (let* ((raw-args (cdr (command-line))) ; skip binary name
209 ;; Filter out sigil CLI noise from (command-line):
210 ;; -L <path>, --no-auto-compile, script filenames, "run", and "--"
211 (args (let loop ((args raw-args) (result '()))
212 (cond
213 ((null? args) (reverse result))
214 ;; Skip -L and its argument
215 ((and (string=? (car args) "-L") (pair? (cdr args)))
216 (loop (cddr args) result))
217 ;; Skip --no-auto-compile
218 ((string=? (car args) "--no-auto-compile")
219 (loop (cdr args) result))
220 ;; Skip script filename
221 ((string-ends-with? (car args) ".sgl")
222 (loop (cdr args) result))
223 ;; Skip "run" subcommand and "--" separator
224 ((or (string=? (car args) "run")
225 (string=? (car args) "--"))
226 (loop (cdr args) result))
227 (else (loop (cdr args) (cons (car args) result))))))
228 (cmd (if (null? args) "build" (car args)))
229 (opts (if (null? args) '() (cdr args))))
230 (cond
231 ((equal? cmd "build")
232 (run-build site-spec opts))
233 ((equal? cmd "watch")
234 (run-watch site-spec opts))
235 ((equal? cmd "serve")
236 (run-serve site-spec opts))
237 ((or (equal? cmd "help") (equal? cmd "--help") (equal? cmd "-h"))
238 (show-publish-help))
239 (else
240 (eprintln "~a Unknown command: ~a" (a:red "Error:") cmd)
241 (show-publish-help)
242 (exit 1)))))
244 ;; Run build command - one-shot site generation
245 (define (run-build site-spec opts)
246 (let* ((config (dict-ref site-spec config:))
247 (pipeline (dict-ref site-spec pipeline:))
248 (output-dir (dict-ref config output-dir: "out"))
249 (output-dir* (parse-output-option opts output-dir)))
250 (println "")
251 (println "~a Building site..." (a:bold "press"))
252 (println " Output: ~a" output-dir*)
253 (println "")
255 (let* ((config* (dict-set config output-dir: output-dir*))
256 (start-time (current-milliseconds))
257 (graph (build-graph config* pipeline)))
258 (println " Scanned ~a content items" (length (state-items graph)))
259 (println " Scanned ~a assets" (length (state-assets graph)))
260 (println "")
261 (println " Executing graph...")
262 (flush-output-port (current-output-port))
263 (execute-graph graph)
264 (let ((elapsed (- (current-milliseconds) start-time)))
265 (println "~a Built in ~ams" (a:green "Done!") elapsed)))))
267 ;; Run watch command - rebuild on file changes
268 (define (run-watch site-spec opts)
269 (let* ((config (dict-ref site-spec config:))
270 (pipeline (dict-ref site-spec pipeline:))
271 (output-dir (dict-ref config output-dir: "out"))
272 (output-dir* (parse-output-option opts output-dir))
273 (watch-patterns (dict-ref site-spec watch-patterns: '()))
274 (source-dirs (extract-source-dirs watch-patterns)))
275 (println "")
276 (println "~a Building site..." (a:bold "press"))
277 (println " Output: ~a" output-dir*)
278 (println "")
280 (let* ((config* (dict-set config output-dir: output-dir*))
281 (start-time (current-milliseconds))
282 (graph (build-graph config* pipeline)))
283 (println " Scanned ~a content items" (length (state-items graph)))
284 (execute-graph graph)
285 (let ((elapsed (- (current-milliseconds) start-time)))
286 (println "~a Built in ~ams" (a:green "Done!") elapsed))
288 ;; Watch loop
289 (println "")
290 (println "Watching for changes... (Ctrl+C to stop)")
291 (watch-loop config* pipeline graph source-dirs 1000))))
293 ;; Watch loop - poll for changes and rebuild
294 (define (watch-loop config pipeline prev-graph source-dirs poll-interval)
295 (let ((w (make-watcher source-dirs))
296 (interval (/ poll-interval 1000)))
297 (let loop ((graph prev-graph))
298 (sleep interval)
299 (let ((changed (watcher-poll w)))
300 (if (pair? changed)
301 (begin
302 (println "[watch] Rebuilding...")
303 (let* ((start (current-milliseconds))
304 (new-graph (build-graph config pipeline)))
305 (execute-changed new-graph graph)
306 (let ((elapsed (- (current-milliseconds) start)))
307 (println "[watch] Rebuilt in ~ams" elapsed))
308 (loop new-graph)))
309 (loop graph))))))
311 ;; Run serve command - dev server with live reload
312 (define (run-serve site-spec opts)
313 (let* ((config (dict-ref site-spec config:))
314 (pipeline (dict-ref site-spec pipeline:))
315 (output-dir (dict-ref config output-dir: "out"))
316 (output-dir* (parse-output-option opts output-dir))
317 (port (parse-port-option opts 8080))
318 (watch-patterns (dict-ref site-spec watch-patterns: '()))
319 (source-dirs (extract-source-dirs watch-patterns))
320 (config* (dict-set config output-dir: output-dir*)))
321 ;; Use dev-server from sigil-publish
322 (dev-server-start
323 (dev-server
324 config: config*
325 pipeline: pipeline
326 port: port
327 output-dir: output-dir*
328 source-dirs: source-dirs))))
330 ;; Parse --output option from args
331 (define (parse-output-option opts default)
332 (let loop ((opts opts))
333 (cond
334 ((null? opts) default)
335 ((and (or (equal? (car opts) "-o")
336 (equal? (car opts) "--output"))
337 (pair? (cdr opts)))
338 (cadr opts))
339 (else (loop (cdr opts))))))
341 ;; Parse --port option from args
342 (define (parse-port-option opts default)
343 (let loop ((opts opts))
344 (cond
345 ((null? opts) default)
346 ((and (or (equal? (car opts) "-p")
347 (equal? (car opts) "--port"))
348 (pair? (cdr opts)))
349 (or (string->number (cadr opts)) default))
350 (else (loop (cdr opts))))))
352 ;; Extract base directories from glob patterns
353 (define (extract-source-dirs patterns)
354 (filter-map
355 (lambda (pattern)
356 (let* ((parts (string-split pattern "/"))
357 (first-part (if (null? parts) "" (car parts))))
358 (if (and (string? first-part)
359 (> (string-length first-part) 0)
360 (not (string-index first-part (lambda (c) (eq? c #\*)))))
361 first-part
362 #f)))
363 patterns))
365 ;; Show help
366 (define (show-publish-help)
367 (println "")
368 (println "~a" (a:bold "Usage:"))
369 (println " press <command> [options]")
370 (println "")
371 (println "~a" (a:bold "Commands:"))
372 (println " build Build site (default)")
373 (println " watch Watch for changes and rebuild")
374 (println " serve Dev server with live reload")
375 (println " help Show this help")
376 (println "")
377 (println "~a" (a:bold "Options:"))
378 (println " -o, --output DIR Output directory")
379 (println " -p, --port PORT Server port (default: 8080)")
380 (println ""))
382 ;; ============================================================
383 ;; Helpers
384 ;; ============================================================
386 ;; Extract keyword argument from args list
387 (define (extract-keyword-arg args key default)
388 (let loop ((args args))
389 (cond
390 ((null? args) default)
391 ((and (keyword? (car args))
392 (eq? (car args) key)
393 (pair? (cdr args)))
394 (cadr args))
395 (else (loop (cdr args))))))))