You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
162 lines
7.3 KiB
Racket
162 lines
7.3 KiB
Racket
#lang debug racket/base
|
|
(require racket/runtime-path
|
|
racket/string
|
|
racket/path
|
|
racket/match
|
|
racket/list
|
|
fontland/font-path
|
|
"attrs.rkt")
|
|
(provide (all-defined-out))
|
|
|
|
(define-runtime-path quadwriter-fonts-dir "fonts")
|
|
(define-runtime-path default-font-face "fonts/default/SourceSerifPro-Regular.otf")
|
|
(define default-font-family "text")
|
|
(define default-font-size 12)
|
|
(define default-line-height 16)
|
|
(define default-font-color "black")
|
|
(define default-font-features "")
|
|
|
|
(define font-paths (make-hash))
|
|
|
|
(define top-font-directory "fonts")
|
|
(define base-extensions '(".otf" ".ttf" ".woff"))
|
|
(define font-file-extensions (map string->bytes/utf-8 (append base-extensions (map string-upcase base-extensions))))
|
|
|
|
(define (fonts-in-directory dir)
|
|
(for/list ([font-path (in-directory dir)]
|
|
#:when (member (path-get-extension font-path) font-file-extensions))
|
|
font-path))
|
|
|
|
(define (setup-font-path-table! base-path)
|
|
;; create a table of font paths that we can use to resolve references to font names.
|
|
|
|
;; rules for font naming
|
|
;; "fonts" subdirectory on top
|
|
;; family directories inside: each named with font family name
|
|
;; this makes it possible to give font families generic names (e.g., "body-text")
|
|
;; and change the font files without disturbing anything else.
|
|
(hash-clear! font-paths)
|
|
(define doc-fonts-dir
|
|
(simple-form-path
|
|
(build-path (match/values (split-path base-path)
|
|
[(base name #true) (build-path base name)]
|
|
[(dir _ _) dir]) top-font-directory)))
|
|
;; run doc-fonts-dir first because earlier fonts take precedence (using hash-ref! below)
|
|
(for* ([fonts-dir (in-list (list doc-fonts-dir quadwriter-fonts-dir))]
|
|
#:when (directory-exists? fonts-dir)
|
|
[font-family-subdir (in-directory fonts-dir)]
|
|
#:when (directory-exists? font-family-subdir)
|
|
[fonts-in-this-directory (in-value (fonts-in-directory font-family-subdir))]
|
|
[font-path (in-list fonts-in-this-directory)])
|
|
(match-define (list font-path-string family-name)
|
|
(for/list ([x (list font-path font-family-subdir)])
|
|
(path->string (find-relative-path fonts-dir x))))
|
|
(define path-parts (for/list ([part (in-list (explode-path (string->path (string-downcase font-path-string))))])
|
|
(path->string part)))
|
|
(define key
|
|
(cons (string-downcase family-name)
|
|
(cond
|
|
;; special case: if there's only one style in the family directory,
|
|
;; treat it as the regular style, regardless of name
|
|
[(= (length fonts-in-this-directory) 1) 'r]
|
|
;; cases where fonts are in subdirectories named by style
|
|
;; infer style from subdir name
|
|
[(member "bold-italic" path-parts) 'bi]
|
|
[(member "bold" path-parts) 'b]
|
|
[(member "italic" path-parts) 'i]
|
|
[else
|
|
;; try to infer from filename alone
|
|
;; TODO: what happens when there is no regular style?
|
|
(define filename (string-downcase (last path-parts)))
|
|
(define filename-contains-bold? (string-contains? filename "bold"))
|
|
(define filename-contains-italic? (string-contains? filename "italic"))
|
|
(cond
|
|
[(and filename-contains-bold? filename-contains-italic?) 'bi]
|
|
[filename-contains-bold? 'b]
|
|
[filename-contains-italic? 'i]
|
|
[else 'r])])))
|
|
;; only set value if there's not one there already.
|
|
;; this means that we only use the first eligible font we find.
|
|
(hash-ref! font-paths key font-path)))
|
|
|
|
(define (make-key font-family [bold #f] [italic #f])
|
|
(cons (string-downcase font-family)
|
|
(cond
|
|
[(and bold italic) 'bi]
|
|
[bold 'b]
|
|
[italic 'i]
|
|
[else 'r])))
|
|
|
|
(define (font-attrs->path font-family bold italic)
|
|
;; find the font-path corresponding to a certain family name and style.
|
|
(define regular-key (make-key font-family))
|
|
|
|
;; if it's not already in font paths, it might be a system font
|
|
;; we use `family->path` to try to resolve the ribbi styles
|
|
;; if the font is a system font, we will end up with paths.
|
|
;; if it's not, we will end up with #false for those entries in `font-paths`,
|
|
;; and fall through to the default font when we do the `cond` below.
|
|
(unless (hash-has-key? font-paths regular-key)
|
|
(for* ([bold (in-list (list #false #true))]
|
|
[italic (in-list (list #false #true))])
|
|
(hash-set! font-paths
|
|
(make-key font-family bold italic)
|
|
(family->path font-family #:bold bold #:italic italic))))
|
|
(cond
|
|
[(hash-ref font-paths (make-key font-family bold italic) #false)]
|
|
;; try regular style if style-specific key isn't there for b i or bi
|
|
[(and (or bold italic) (hash-ref font-paths regular-key #false))]
|
|
;; otherwise use default
|
|
[else default-font-face]))
|
|
|
|
(define (font-path-string? x)
|
|
(and (path-string? x)
|
|
(member (path-get-extension (string->path x)) font-file-extensions)
|
|
#true))
|
|
|
|
(module+ test
|
|
(require rackunit)
|
|
(check-true (font-path-string? "foo.woff"))
|
|
(check-true (font-path-string? "foo.otf"))
|
|
(check-true (font-path-string? "foo.ttf"))
|
|
(check-false (font-path-string? "foo.woffy"))
|
|
(check-false (font-path-string? "foo")))
|
|
|
|
|
|
(define (resolve-font-path! attrs)
|
|
;; convert references to a font family and style to an font path on disk
|
|
;; we trust it exists because we used `setup-font-path-table!` earlier,
|
|
;; but if not, fallback fonts will kick in, on the idea that a missing font shouldn't stop the show
|
|
(define this-font-family (hash-ref! attrs :font-family default-font-family))
|
|
(hash-set! attrs :font-path
|
|
(match this-font-family
|
|
[(? font-path-string? ps) (path->complete-path ps)]
|
|
[_
|
|
(define this-bold (hash-ref! attrs :font-bold #false))
|
|
(define this-italic (hash-ref! attrs :font-italic #false))
|
|
(font-attrs->path this-font-family this-bold this-italic)])))
|
|
|
|
(define (parse-em pstr)
|
|
(define em-suffix "em")
|
|
(and
|
|
pstr
|
|
(string? pstr)
|
|
(string-suffix? pstr em-suffix)
|
|
(string->number (string-trim pstr em-suffix))))
|
|
|
|
(define (resolve-font-size! attrs)
|
|
;; convert font-size attributes into a simple font size
|
|
;; we stashed the previous size in private key 'font-size-previous
|
|
(define prev-font-size-key 'font-size-previous)
|
|
(define val (parse-dimension (hash-ref attrs :font-size default-font-size)))
|
|
(define adjustment (parse-em val))
|
|
;; if our value represents an adjustment, we apply the adjustment to the previous value
|
|
;; otherwise we use our value directly
|
|
(define base-size (if adjustment (hash-ref attrs prev-font-size-key default-font-size) val))
|
|
(define base-size-adjusted (and base-size (* base-size (or adjustment 1))))
|
|
;; we write our new value into both font-size and font-size-previous
|
|
;; because as we cascade down, we're likely to come across superseding values
|
|
;; of font-size (but font-size-previous will persist)
|
|
(hash-set! attrs :font-size base-size-adjusted)
|
|
(hash-set! attrs prev-font-size-key base-size-adjusted))
|