AtlatestRepositorypress
1
;;; (press) - Static Site Generation Library2
;;;3
;;; A composable pipeline for generating websites from various content4
;;; formats. Library-first design with functions and dicts.5
;;;6
;;; ## Quick Start7
;;;8
;;; ```scheme9
;;; (import (press))10
;;;11
;;; ;; Build a site12
;;; (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
;;; ## Architecture18
;;;19
;;; Two-phase execution for memory efficiency:20
;;; 1. Build Graph: Scan sources, create items with producers21
;;; 2. Execute: Call producers to render content22
;;;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
(export44
;; High-level site builder45
site47
;; Script runner48
run-publish50
;; Pipeline execution (re-export from core)51
build-graph52
execute-graph53
execute-changed55
;; Pipeline stages (re-export)56
scan-files57
scan-assets58
filter-drafts59
build-navigation60
build-ref-index61
build-path-index62
attach-content-producers63
attach-asset-producers64
rewrite-links65
resolve-refs67
;; Content generators (re-export from generate)68
collection-page69
sxml-page71
;; Feeds (re-export from feeds)72
with-feeds74
;; Theme support (re-export)75
default-theme76
sxml->html78
;; Transforms (re-export)79
syntax-highlight81
;; State accessors (re-export)82
state-items83
state-assets84
state-navigation85
state-ref-index86
state-path-index88
;; Documentation publishing (re-export from docs)89
docs-theme90
generate-docs-css91
scan-api-docs92
scan-library-docs93
lib-index-template94
module-details-template95
export-details->sxml96
syntax-details->sxml97
format-library-name98
module-details->output-path)100
(begin102
;; ============================================================103
;; High-Level Site Builder104
;; ============================================================106
;;; Create a site configuration.107
;;;108
;;; Returns a dict with:109
;;; - config: Configuration dict for the pipeline110
;;; - pipeline: List of pipeline stages111
;;; - watch-patterns: List of glob patterns to watch for changes112
;;;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 pipeline116
;;; - Omitted: Use auto-generated pipeline from content/assets117
;;;118
;;; ```scheme119
;;; ;; Simple site with defaults120
;;; (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-drafts129
;;; build-navigation130
;;; attach-content-producers))131
;;;132
;;; ;; Modify default pipeline133
;;; (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 (cond150
((null? content-patterns) '())151
((string? content-patterns) (list content-patterns))152
(else content-patterns)))153
;; Default pipeline built from content/assets154
(default-pipeline155
(append156
(map scan-files content-list)157
(if asset-pattern158
(list (scan-assets asset-pattern))159
'())160
(list filter-drafts161
build-navigation162
build-ref-index163
build-path-index164
attach-content-producers165
attach-asset-producers)))166
;; Final pipeline: custom list, modified default, or default167
(final-pipeline168
(cond169
((list? custom-pipeline) custom-pipeline)170
((procedure? custom-pipeline) (custom-pipeline default-pipeline))171
(else default-pipeline)))172
;; Watch patterns173
(default-watch (append content-list174
(if asset-pattern (list asset-pattern) '())))175
(final-watch (or custom-watch default-watch)))176
#{config: #{title: title177
base-url: base-url178
output-dir: output-dir179
theme: theme180
transforms: transforms181
options: #{draft-mode: draft-mode}}182
pipeline: final-pipeline183
watch-patterns: final-watch})))185
;; ============================================================186
;; Script Runner187
;; ============================================================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 generation193
;;; - watch: Rebuild on file changes (no server)194
;;; - serve: HTTP server + watch + live reload195
;;;196
;;; Usage in scripts:197
;;; ```scheme198
;;; (import (press))199
;;;200
;;; (define my-site201
;;; (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 name209
;; 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
(cond213
((null? args) (reverse result))214
;; Skip -L and its argument215
((and (string=? (car args) "-L") (pair? (cdr args)))216
(loop (cddr args) result))217
;; Skip --no-auto-compile218
((string=? (car args) "--no-auto-compile")219
(loop (cdr args) result))220
;; Skip script filename221
((string-ends-with? (car args) ".sgl")222
(loop (cdr args) result))223
;; Skip "run" subcommand and "--" separator224
((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
(cond231
((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
(else240
(eprintln "~a Unknown command: ~a" (a:red "Error:") cmd)241
(show-publish-help)242
(exit 1)))))244
;; Run build command - one-shot site generation245
(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 changes268
(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 loop289
(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 rebuild294
(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
(begin302
(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 reload312
(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-publish322
(dev-server-start323
(dev-server324
config: config*325
pipeline: pipeline326
port: port327
output-dir: output-dir*328
source-dirs: source-dirs))))330
;; Parse --output option from args331
(define (parse-output-option opts default)332
(let loop ((opts opts))333
(cond334
((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 args342
(define (parse-port-option opts default)343
(let loop ((opts opts))344
(cond345
((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 patterns353
(define (extract-source-dirs patterns)354
(filter-map355
(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-part362
#f)))363
patterns))365
;; Show help366
(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
;; Helpers384
;; ============================================================386
;; Extract keyword argument from args list387
(define (extract-keyword-arg args key default)388
(let loop ((args args))389
(cond390
((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))))))))