#lang debug racket/base (require racket/match racket/hash racket/list racket/string racket/set "dimension.rkt" "pipeline.rkt" "struct.rkt" "constants.rkt" "quad.rkt" "param.rkt") (provide (all-defined-out)) (define (do-attr-iteration qs #:which-attr [which-attr #false] #:attr-proc attr-proc) (define attr-predicate (match which-attr [#false (λ (ak av) #true)] [(? attr-key? attr-key) (λ (ak av) (eq? ak attr-key))] [(? procedure? pred) (if (eq? 1 (procedure-arity pred)) (λ (ak _) (pred ak)) ; 1 arity implies key-only test pred)] [other (raise-argument-error 'do-attr-iteration "key predicate" other)])) (define attrs-seen (mutable-seteq)) (let loop ([xs qs]) (for ([x (in-list xs)] #:when (quad? x)) (let* ([q x] [attrs (quad-attrs q)]) (unless (set-member? attrs-seen attrs) (for ([(ak av) (in-hash attrs)] #:when (attr-predicate ak av)) (match (attr-proc ak av attrs) ;; void value: do nothing [(? void?) (void)] ;; otherwise treat return value as new attr value [new-av (hash-set! attrs ak new-av)])) (set-add! attrs-seen attrs)) (loop (quad-elems q)))))) (define-pass (upgrade-attr-keys qs) ;; convert attr keys from symbols to attr struct types ;; also lets us validate keys strictly, if we want #:pre (list-of quad?) #:post (list-of quad?) (define attr-lookup-table (for/hasheq ([a (in-list (current-attrs))]) (values (attr-key-name a) a))) (define strict-attrs? (current-strict-attrs?)) (define (do-upgrade ak av attrs) (cond [(attr-key? ak) av] [(symbol? ak) (match (hash-ref attr-lookup-table ak :unknown-key) [(== :unknown-key eq?) #:when strict-attrs? (raise-argument-error 'upgrade-attr-keys "known attr" ak)] [attr-key (hash-remove! attrs ak) (hash-set! attrs attr-key av)])] [else (raise-argument-error 'upgrade-attr-keys "symbol or attr" ak)])) (do-attr-iteration qs #:attr-proc do-upgrade)) (define-pass (downcase-attr-values qs) ;; make attribute values lowercase, unless they're case-sensitive ;; so we can check them more easily later. ;; in principle we could do this earlier and recursively process a single quad ;; rather than linearized quads ;; it would be faster because there are fewer copies of the attr hashes, ;; so we do fewer tests ;; but let's stay with the pipeline policy of operating on flat lists of quads #:pre (list-of quad?) #:post (list-of quad?) (do-attr-iteration qs #:which-attr attr-cased-string-key? #:attr-proc (λ (ak av attrs) (string-downcase av)))) (define-pass (convert-boolean-attr-values qs) #:pre (list-of quad?) #:post (list-of quad?) (do-attr-iteration qs #:which-attr attr-boolean-key? #:attr-proc (λ (ak av attrs) (match (string-downcase av) ["false" #false] [_ #true])))) (define-pass (convert-numeric-attr-values qs) #:pre (list-of quad?) #:post (list-of quad?) (do-attr-iteration qs #:which-attr attr-numeric-key? #:attr-proc (λ (ak av attrs) (or (string->number av) (raise-argument-error 'convert-numeric-attr-values "numeric string" av))))) (define-pass (complete-attr-paths qs) #:pre (list-of quad?) #:post (list-of quad?) ;; convert every pathlike thing to a complete path (well, path string, because it's inside an attr) ;; so we don't get tripped up later by relative paths ;; relies on `current-directory` being parameterized to source file's dir (do-attr-iteration qs #:which-attr attr-path-key? #:attr-proc (λ (ak av attrs) (path->complete-path av)))) (define-pass (parse-dimension-strings qs) #:pre (list-of quad?) #:post (list-of quad?) ;; certain attributes can be "dimension strings", which are strings like "3in" or "4.2cm" ;; we parse them into the equivalent measurement in points. (do-attr-iteration qs #:which-attr attr-dimension-string-key? #:attr-proc (λ (ak av attrs) (parse-dimension av attrs)))) (module+ test (require rackunit) (define-attr-list debug-attrs [:foo (attr-cased-string-key 'foo)] [:ps (attr-path-key 'ps)] [:dim (attr-dimension-string-key 'dim)] [:boolt (attr-boolean-key 'bool)] [:boolf (attr-boolean-key 'bool)] [:num (attr-numeric-key 'num)]) (parameterize ([current-attrs debug-attrs]) (define (make-q) (make-quad #:attrs (make-hasheq (list (cons :foo "BAR") (cons 'ding "dong") (cons :ps "file.txt") (cons :dim "2in") (cons :boolt "true") (cons :boolf "false") (cons :num "42.5"))))) (define qs (list (make-q))) (check-exn exn? (λ () (parameterize ([current-strict-attrs? #true]) (upgrade-attr-keys (list (make-q)))))) (check-not-exn (λ () (parameterize ([current-strict-attrs? #false]) (upgrade-attr-keys (list (make-q)))))) (check-equal? (quad-ref (car (downcase-attr-values qs)) :foo) "bar") (check-true (complete-path? (quad-ref (car (complete-attr-paths qs)) :ps))) (check-equal? (quad-ref (car (parse-dimension-strings qs)) :dim) 144) (let ([q (car (convert-boolean-attr-values qs))]) (check-true (quad-ref q :boolt)) (check-false (quad-ref q :boolf))) (check-equal? (quad-ref (car (convert-numeric-attr-values qs)) :num) 42.5)))