private/emitter.rkt
;;;;;; emitter.rkt - YAML emitter.    -*- Mode: Racket -*-

#lang racket

(require
 srfi/13
 "errors.rkt"
 "events.rkt"
 "utils.rkt")

(provide make-emitter)

(define (emitter-error message)
  (error 'emitter message))

(struct scalar-analysis
  (scalar empty multiline allow-flow-plain allow-block-plain
          allow-single-quoted allow-double-quoted allow-block))

(define (make-emitter [out (current-output-port)]
                      #:canonical [canonical #f]
                      #:indent [default-indent #f]
                      #:width [default-width #f]
                      #:allow-unicode [allow-unicode #f]
                      #:line-break [line-break #f])
  (define DEFAULT-TAG-PREFIXES
    #hash(("!" . "!") ("tag:yaml.org,2002:" . "!!")))

  (define encoding #f)
  (define states '())
  (define (state) (expect-stream-start))
  (define events '())
  (define event #f)
  (define indents '())
  (define indent #f)
  (define flow-level 0)
  (define root-context #f)
  (define sequence-context #f)
  (define mapping-context #f)
  (define simple-key-context #f)
  (define line 0)
  (define column 0)
  (define whitespace #t)
  (define indention #t)
  (define open-ended #f)
  (define best-indent
    (if (and default-indent
             (< 1 default-indent 10))
        default-indent
        2))
  (define best-width
    (if (and default-width
             (> default-width (* 2 best-indent)))
        default-width
        80))
  (define best-line-break
    (if (member line-break '("\r" "\n" "\r\n"))
        line-break
        (string #\newline)))
  (define tag-prefixes #f)
  (define prepared-anchor #f)
  (define prepared-tag #f)
  (define analysis #f)
  (define style #f)

  (define (dispose)
    ;; Reset the state attributes (to clear self-references)
    (set! states '())
    (set! state #f))

  (define (emit evt)
    (append! events (list evt))
    (while (not (need-more-events?))
      (set! event (car events))
      (set! events (cdr events))
      (state)
      (set! event #f)))

  ;; In some cases, we wait for a few next events before emitting.

  (define (need-more-events?)
    (cond
     [(null? events) #t]
     [(document-start-event? (car events))
      (need-events? 1)]
     [(sequence-start-event? (car events))
      (need-events? 2)]
     [(mapping-start-event? (car events))
      (need-events? 3)]
     [else #f]))

  (define (need-events? count)
    (let loop ([level 0] [es (cdr events)])
      (if (null? es)
          (< (length events) (add1 count))
          (let ([e (car es)])
            (cond
             [(or (document-start-event? e)
                  (collection-start-event? e))
              (set! level (add1 level))]
             [(or (document-end-event? e)
                  (collection-end-event? e))
              (set! level (sub1 level))]
             [(stream-end-event? e)
              (set! level -1)])
            (and (>= level 0)
                 (loop level (cdr es)))))))

  (define (increase-indent [flow #f] [indentless #f])
    (append! indents (list indent))
    (if (eq? #f indent)
        (set! indent (if flow best-indent 0))
        (unless indentless
          (set! indent (+ indent best-indent)))))

  ;; States.

  ;; Stream handlers.

  (define (expect-stream-start)
    (cond
     [(stream-start-event? event)
      (write-stream-start)
      (set! state expect-first-document-start)]
     [else
      (emitter-error
       (format "expected stream-start, but got ~a"
               (event->string event)))]))

  (define (expect-nothing)
    (emitter-error
     (format "expected nothing, but got ~a"
             (event->string event))))

  ;; Document handlers.

  (define (expect-first-document-start)
    (expect-document-start #t))

  (define (expect-document-start [first #f])
    (cond
      [(document-start-event? event)
       (when (and (or (document-start-event-version event)
                      (document-start-event-tags event))
                  open-ended)
         (write-indicator "..." #t)
         (write-indent))
       (when (document-start-event-version event)
         (write-version-directive
          (prepare-version (document-start-event-version event))))
       (set! tag-prefixes (hash-copy DEFAULT-TAG-PREFIXES))
       (when (document-start-event-tags event)
         (let ([tags (document-start-event-tags event)])
           (for ([handle (sort (hash-keys tags) string<?)])
             (let ([prefix (hash-ref tags handle)])
               (hash-set! tag-prefixes prefix handle)
               (let* ([handle-text (prepare-tag-handle handle)]
                      [prefix-text (prepare-tag-prefix prefix)])
                 (write-tag-directive handle-text prefix-text))))))
       (unless (and first (not (document-start-event-explicit event))
                    (not canonical) (not (document-start-event-version event))
                    (or (not (hash? (document-start-event-tags event)))
                        (null? (hash-keys (document-start-event-tags event))))
                    (not (check-empty-document?)))
         (write-indent)
         (write-indicator "---" #t)
         (when canonical
           (write-indent)))
       (set! state expect-document-root)]
      [(stream-end-event? event)
       (when open-ended
         (write-indicator "..." #t)
         (write-indent))
       (write-stream-end)
       (set! state expect-nothing)]
      [else
       (emitter-error
        (format "expected document-start, but got ~a"
                (event->string event)))]))

  (define (expect-document-end)
    (cond
     [(document-end-event? event)
      (write-indent)
      (when (document-end-event-explicit event)
        (write-indicator "..." #t)
        (write-indent))
      (flush-stream)
      (set! state expect-document-start)]
     [else
      (emitter-error
       (format "expected document-end, but got ~a"
               (event->string event)))]))

  (define (expect-document-root)
    (append! states (list expect-document-end))
    (expect-node #t #f #f #f))

  ;; Node handlers.

  (define (expect-node [root #f] [sequence #f] [mapping #f] [simple-key #f])
    (set! root-context root)
    (set! sequence-context sequence)
    (set! mapping-context mapping)
    (set! simple-key-context simple-key)
    (cond
      [(alias-event? event)
       (expect-alias)]
      [(or (scalar-event? event)
           (collection-start-event? event))
       (process-anchor "&")
       (process-tag)
       (cond
         [(scalar-event? event)
          (expect-scalar)]
         [(sequence-start-event? event)
          (if (or (> flow-level 0)
                  canonical
                  (sequence-start-event-flow-style event)
                  (check-empty-sequence?))
              (expect-flow-sequence)
              (expect-block-sequence))]
         [(mapping-start-event? event)
          (if (or (> flow-level 0)
                  canonical
                  (mapping-start-event-flow-style event)
                  (check-empty-mapping?))
              (expect-flow-mapping)
              (expect-block-mapping))])]
      [else
       (emitter-error
        (format "expected node, but got ~a"
                (event->string event)))]))

  (define (expect-alias)
    (unless (alias-event-anchor event)
      (emitter-error "anchor is not specified for alias"))
    (process-anchor "*")
    (set! state (pop! states)))

  (define (expect-scalar)
    (increase-indent #t)
    (process-scalar)
    (set! indent (pop! indents))
    (set! state (pop! states)))

  ;; Flow sequence handlers.

  (define (expect-flow-sequence)
    (write-indicator "[" #t #t)
    (set! flow-level (add1 flow-level))
    (increase-indent #t)
    (set! state expect-first-flow-sequence-item))

  (define (expect-first-flow-sequence-item)
    (cond
     [(sequence-end-event? event)
      (set! indent (pop! indents))
      (set! flow-level (sub1 flow-level))
      (write-indicator "]" #f)
      (set! state (pop! states))]
     [else
      (when (or canonical (> column best-width))
        (write-indent))
      (append! states (list expect-flow-sequence-item))
      (expect-node #f #t #f #f)]))

  (define (expect-flow-sequence-item)
    (cond
     [(sequence-end-event? event)
      (set! indent (pop! indents))
      (set! flow-level (sub1 flow-level))
      (when canonical
        (write-indicator "," #f)
        (write-indent))
      (write-indicator "]" #f)
      (set! state (pop! states))]
     [else
      (write-indicator "," #f)
      (when (or canonical (> column best-width))
        (write-indent))
      (append! states (list expect-flow-sequence-item))
      (expect-node #f #t #f #f)]))

  ;; Flow mapping handlers.

  (define (expect-flow-mapping)
    (write-indicator "{" #t #t)
    (set! flow-level (add1 flow-level))
    (increase-indent #t)
    (set! state expect-first-flow-mapping-key))

  (define (expect-first-flow-mapping-key)
    (cond
     [(mapping-end-event? event)
      (set! indent (pop! indents))
      (set! flow-level (sub1 flow-level))
      (write-indicator "}" #f)
      (set! state (pop! states))]
     [else
       (when (or canonical (> column best-width))
         (write-indent))
       (cond
        [(and (not canonical) (check-simple-key?))
         (append! states (list expect-flow-mapping-simple-value))
         (expect-node #f #f #t #t)]
        [else
         (write-indicator "?" #t)
         (append! states (list expect-flow-mapping-value))
         (expect-node #f #f #t #f)])]))

  (define (expect-flow-mapping-key)
    (cond
     [(mapping-end-event? event)
      (set! indent (pop! indents))
      (set! flow-level (sub1 flow-level))
      (when canonical
        (write-indicator "," #f)
        (write-indent))
      (write-indicator "}" #f)
      (set! state (pop! states))]
     [else
      (write-indicator "," #f)
      (when (or canonical (> column best-width))
        (write-indent))
      (cond
       [(and (not canonical) (check-simple-key?))
        (append! states (list expect-flow-mapping-simple-value))
        (expect-node #f #f #t #t)]
       [else
        (write-indicator "?" #t)
        (append! states (list expect-flow-mapping-value))
        (expect-node #f #f #t #f)])]))

  (define (expect-flow-mapping-simple-value)
    (write-indicator ":" #f)
    (append! states (list expect-flow-mapping-key))
    (expect-node #f #f #t #f))

  (define (expect-flow-mapping-value)
    (when (or canonical (> column best-width))
      (write-indent))
    (write-indicator ":" #t)
    (append! states (list expect-flow-mapping-key))
    (expect-node #f #f #t #f))

  ;; Block sequence handlers.

  (define (expect-block-sequence)
    (increase-indent #f (and mapping-context (not indention)))
    (set! state expect-first-block-sequence-item))

  (define (expect-first-block-sequence-item)
    (expect-block-sequence-item #t))

  (define (expect-block-sequence-item [first #f])
    (cond
     [(and (not first) (sequence-end-event? event))
      (set! indent (pop! indents))
      (set! state (pop! states))]
     [else
      (write-indent)
      (write-indicator "-" #t #f #t)
      (append! states (list expect-block-sequence-item))
      (expect-node #f #t #f #f)]))

  ;; Block mapping handlers.

  (define (expect-block-mapping)
    (increase-indent #f)
    (set! state expect-first-block-mapping-key))

  (define (expect-first-block-mapping-key)
    (expect-block-mapping-key #t))

  (define (expect-block-mapping-key [first #f])
    (cond
     [(and (not first) (mapping-end-event? event))
      (set! indent (pop! indents))
      (set! state (pop! states))]
     [else
      (write-indent)
      (cond
       [(check-simple-key?)
        (append! states (list expect-block-mapping-simple-value))
        (expect-node #f #f #t #t)]
       [else
        (write-indicator "?" #t #f #t)
        (append! states (list expect-block-mapping-value))
        (expect-node #f #f #t #f)])]))

  (define (expect-block-mapping-simple-value)
    (write-indicator ":" #f)
    (append! states (list expect-block-mapping-key))
    (expect-node #f #f #t #f))

  (define (expect-block-mapping-value)
    (write-indent)
    (write-indicator ":" #t #f #t)
    (append! states (list expect-block-mapping-key))
    (expect-node #f #f #t #f))
  
  ;; Checkers.
  ;;  (expect-node [root #f] [sequence #f] [mapping #f] [simple-key #f])

  (define (check-empty-sequence?)
    (and (sequence-start-event? event)
         (not (null? events))
         (sequence-end-event? (car events))))

  (define (check-empty-mapping?)
    (and (mapping-start-event? event)
         (not (null? events))
         (mapping-end-event? (car events))))

  (define (check-empty-document?)
    (and (document-start-event? event)
         (not (null? events))
         (let ([e (car events)])
           (and (scalar-event? e)
                (not (scalar-event-anchor e))
                (not (scalar-event-tag e))
                (scalar-event-implicit e)
                (equal? "" (scalar-event-value e))))))

  (define (check-simple-key?)
    (let ([len 0])
      (when (and (node-event? event)
                 (node-event-anchor event))
        (unless prepared-anchor
          (set! prepared-anchor
                (prepare-anchor (node-event-anchor event))))
        (set! len (+ len (string-length prepared-anchor))))
      (when (and (or (scalar-event? event)
                     (collection-start-event? event))
                 (any-event-tag event))
        (unless prepared-tag
          (set! prepared-tag
                (prepare-tag (any-event-tag event))))
        (set! len (+ len (string-length prepared-tag))))
      (when (scalar-event? event)
        (unless analysis
          (set! analysis (analyze-scalar (scalar-event-value event))))
        (set! len (+ len (string-length (scalar-analysis-scalar analysis)))))
      (or (and (< len 128)
               (alias-event? event))
          (and (scalar-event? event)
               (not (scalar-analysis-empty analysis))
               (not (scalar-analysis-multiline analysis)))
          (check-empty-sequence?)
          (check-empty-mapping?))))

  ;; Anchor, Tag, and Scalar processors.

  (define (process-anchor indicator)
    (cond
     [(not (any-event-anchor event))
      (set! prepared-anchor #f)]
     [else
      (unless prepared-anchor
        (set! prepared-anchor
              (prepare-anchor (any-event-anchor event))))
      (when prepared-anchor
        (write-indicator (format "~a~a" indicator prepared-anchor) #t))
      (set! prepared-anchor #f)]))

  (define (process-tag)
    (let ([tag (any-event-tag event)])
      (when (and (scalar-event? event)
                 (not style))
        (set! style (choose-scalar-style)))
      (cond
       [(and (scalar-event? event)
             (or (not canonical) (not tag))
             (or (and (equal? "" style)
                      (car (scalar-event-implicit event)))
                 (and (not (equal? "" style))
                      (cdr (scalar-event-implicit event)))))
        (set! prepared-tag #f)]
       [(and (not (scalar-event? event))
             (or (not canonical)
                 (not tag))
             (collection-start-event-implicit event))
        (set! prepared-tag #f)]
       [else
        (when (and (scalar-event? event)
                   (car (scalar-event-implicit event))
                   (not tag))
          (set! tag "!")
          (set! prepared-tag #f))
        (unless tag
          (emitter-error "tag is not specified"))
        (unless prepared-tag
          (set! prepared-tag (prepare-tag tag)))
        (when prepared-tag
          (write-indicator prepared-tag #t))
        (set! prepared-tag #f)])))

  (define (choose-scalar-style)
    (unless analysis
      (set! analysis (analyze-scalar (scalar-event-value event))))
    (cond
     [(or (equal? #\" (scalar-event-style event)) canonical)
      #\"]
     [(and (not (scalar-event-style event))
           (car (scalar-event-implicit event))
           (not (and simple-key-context
                     (or (scalar-analysis-empty analysis)
                         (scalar-analysis-multiline analysis))))
           (or (and (> flow-level 0)
                    (scalar-analysis-allow-flow-plain analysis))
               (and (= flow-level 0)
                    (scalar-analysis-allow-block-plain analysis))))
      ""]
     [(and (scalar-event-style event)
           (member (scalar-event-style event) '(#\| #\>))
           (zero? flow-level)
           (not simple-key-context)
           (scalar-analysis-allow-block analysis))
      (scalar-event-style event)]
     [(and (or (not (scalar-event-style event))
               (char=? #\' (scalar-event-style event)))
           (scalar-analysis-allow-single-quoted analysis)
           (not (and simple-key-context
                     (scalar-analysis-multiline analysis))))
      #\']
     [else
      #\"]))

  (define (process-scalar)
    (unless analysis
      (set! analysis (analyze-scalar (scalar-event-value event))))
    (unless style
      (set! style (not simple-key-context)))
    (let ([split (not simple-key-context)])
      (cond
       [(equal? #\" style)
        (write-double-quoted (scalar-analysis-scalar analysis) split)]
       [(equal? #\' style)
        (write-single-quoted (scalar-analysis-scalar analysis) split)]
       [(equal? #\> style)
        (write-folded (scalar-analysis-scalar analysis))]
       [(equal? #\| style)
        (write-literal (scalar-analysis-scalar analysis))]
       [else
        (write-plain (scalar-analysis-scalar analysis) split)])
      (set! analysis #f)
      (set! style #f)))

  ;; Analyzers.

  (define (prepare-version version)
    (match-let ([(cons major minor) version])
      (unless (= 1 major)
        (emitter-error
         (format "unsupported YAML version: ~a.~a" major minor)))
      (format "~a.~a" major minor)))

  (define (prepare-tag-handle handle)
    (unless (> (string-length handle) 0)
      (emitter-error "tag handle must not be empty"))
    (let ([cs (string->list handle)])
      (unless (and (char=? #\! (car cs))
                   (char=? #\! (last cs)))
        (emitter-error
         (format "tag handle must start and end with '!': ~a" handle)))
      (when (> (string-length handle) 1)
        (for ([ch (substring handle 1 (sub1 (string-length handle)))])
          (unless (or (char<=? #\0 ch #\9)
                      (char<=? #\A ch #\Z)
                      (char<=? #\a ch #\z)
                      (char=? #\- ch)
                      (char=? #\_ ch))
            (emitter-error
             (format
              "invalid character ~a in the tag handle: ~a" ch handle)))))
      handle))

  (define (prepare-tag-prefix prefix)
    (unless (> (string-length prefix) 0)
      (emitter-error "tag prefix must not be empty"))
    (let ([chunks '()]
          [start 0]
          [end 0])
      (when (char=? #\! (string-ref prefix 0))
        (set! end 1))
      (while (< end (string-length prefix))
        (let ([ch (string-ref prefix end)])
          (cond
           [(or (char<=? #\0 ch #\9)
                (char<=? #\A ch #\Z)
                (char<=? #\a ch #\z)
                (string-index "-;/?!:@&=+$,_.~*'()[]" ch))
            (set! end (add1 end))]
           [else
            (when (< start end)
              (append! chunks (list (substring prefix start end))))
            (set! start (add1 end))
            (set! end (add1 end))
            (append! chunks (list (format "~a" ch)))])))
      (when (< start end)
        (append! chunks (list (substring prefix start end))))
      (apply string-append chunks)))

  (define (prepare-tag tag)
    (unless (and (string? tag) (> (string-length tag) 0))
      (emitter-error "tag must not be empty"))
    (if (string=? "!" tag)
        tag
        (let ([handle #f]
              [suffix tag])
          (for ([prefix (sort (hash-keys tag-prefixes) string<?)])
            (when (and (string-prefix? prefix tag)
                       (or (string=? "!" prefix)
                           (< (string-length prefix)
                              (string-length tag))))
              (set! handle (hash-ref tag-prefixes prefix))
              (set! suffix (substring tag (string-length prefix)))))
          (let ([chunks '()]
                [start 0]
                [end 0])
            (while (< end (string-length suffix))
              (let ([ch (string-ref suffix end)])
                (cond
                 [(or (char<=? #\0 ch #\9)
                      (char<=? #\A ch #\Z)
                      (char<=? #\a ch #\z)
                      (string-index "-;/?!:@&=+$,_.~*'()[]" ch))
                  (set! end (add1 end))]
                 [else
                  (when (< start end)
                    (append! chunks (list (substring suffix start end))))
                  (set! start (add1 end))
                  (set! end (add1 end))
                  (append! chunks (list (format "~a" ch)))])))
            (when (< start end)
              (append! chunks (list (substring suffix start end))))
            (if (and (string? handle) (> (string-length handle) 0))
                (format "~a~a" handle (apply string-append chunks))
                (format "!<~a>" (apply string-append chunks)))))))

  (define (prepare-anchor anchor)
    (unless (> (string-length anchor) 0)
      (emitter-error "anchor must not be empty"))
    (for ([ch anchor])
      (unless (or (char<=? #\0 ch #\9)
                  (char<=? #\A ch #\Z)
                  (char<=? #\a ch #\z)
                  (char=? #\- ch)
                  (char=? #\_ ch))
        (emitter-error
         (format "invalid character ~a in the anchor: ~a" ch anchor))))
    anchor)

  (define (analyze-scalar scalar)
    (cond
     [(string=? "" scalar)
      (scalar-analysis scalar #t #f #f #t #t #t #f)]
     [else
      (let ([block-indicators #f]
            [flow-indicators #f]
            [line-breaks #f]
            [special-characters #f]
            [leading-space #f]
            [leading-break #f]
            [trailing-space #f]
            [trailing-break #f]
            [break-space #f]
            [space-break #f]
            [preceeded-by-whitespace #t]
            [followed-by-whitespace
             (or (= 1 (string-length scalar))
                 (string-index "\0 \t\r\n\x85\u2028\u2029"
                               (string-ref scalar 1)))]
            [previous-space #f]
            [previous-break #f]
            [index 0]
            [allow-flow-plain #t]
            [allow-block-plain #t]
            [allow-single-quoted #t]
            [allow-double-quoted #t]
            [allow-block #t])
        (when (or (string-prefix? "---" scalar)
                  (string-prefix? "..." scalar))
          (set! block-indicators #t)
          (set! flow-indicators #t))
        (while (< index (string-length scalar))
          (let ([ch (string-ref scalar index)])
            (cond
             [(zero? index)
              (when (string-index "#,[]{}&*!|>'\"%@`" ch)
                (set! flow-indicators #t)
                (set! block-indicators #t))
              (when (string-index "?:" ch)
                (set! flow-indicators #t)
                (when followed-by-whitespace
                  (set! block-indicators #t)))
              (when (and (char=? #\- ch) followed-by-whitespace)
                (set! flow-indicators #t)
                (set! block-indicators #t))]
             [else
              (when (string-index ",?[]{}" ch)
                (set! flow-indicators #t))
              (when (char=? #\: ch)
                (set! flow-indicators #t)
                (when followed-by-whitespace
                  (set! block-indicators #t)))
              (when (and (char=? #\# ch) preceeded-by-whitespace)
                (set! flow-indicators #t)
                (set! block-indicators #t))])
            (when (string-index "\n\x85\u2028\u2029" ch)
              (set! line-breaks #t))
            (unless (or (char=? #\newline ch)
                        (char<=? #\space ch #\~))
              (cond
               [(and (or (char=? #\u0085 ch)
                         (char<=? #\u00A0 ch #\uD7FF)
                         (char<=? #\uE000 ch #\uFFFD))
                     (not (char=? #\uFEFF ch)))
                (unless allow-unicode
                  (set! special-characters #t))]
               [else (set! special-characters #t)]))
            (cond
             [(char=? #\space ch)
              (when (zero? index)
                (set! leading-space #t))
              (when (= index (- (string-length scalar) 1))
                (set! trailing-space #t))
              (when previous-break
                (set! break-space #t))
              (set! previous-space #t)
              (set! previous-break #t)]
             [(string-index "\n\x85\u2028\u2029" ch)
              (when (zero? index)
                (set! leading-break #t))
              (when (= index (- (string-length scalar) 1))
                (set! trailing-break #t))
              (when previous-space
                (set! space-break #t))
              (set! previous-space #f)
              (set! previous-break #t)]
             [else
              (set! previous-space #f)
              (set! previous-break #f)])
            (set! index (add1 index))
            (set! preceeded-by-whitespace
                  (string-index "\0 \t\r\n\x85\u2028\u2029" ch))
            (set! followed-by-whitespace
                  (or (>= (+ 1 index) (string-length scalar))
                      (string-index "\0 \t\r\n\x85\u2028\u2029"
                                    (string-ref scalar (+ index 1)))))))
        (when (or leading-space leading-break
                  trailing-space trailing-break)
          (set! allow-flow-plain #f)
          (set! allow-block-plain #f))
        (when trailing-space
          (set! allow-block #f))
        (when break-space
          (set! allow-flow-plain #f)
          (set! allow-block-plain #f)
          (set! allow-single-quoted #f))
        (when (or space-break special-characters)
          (set! allow-flow-plain #f)
          (set! allow-block-plain #f)
          (set! allow-single-quoted #f)
          (set! allow-block #f))
        (when line-breaks
          (set! allow-flow-plain #f)
          (set! allow-block-plain #f))
        (when flow-indicators
          (set! allow-flow-plain #f))
        (when block-indicators
          (set! allow-block-plain #f))
        (scalar-analysis
         scalar #f line-breaks allow-flow-plain allow-block-plain
         allow-single-quoted allow-double-quoted allow-block))]))
        
  ;; Writers.

  (define (flush-stream)
    (flush-output out))

  (define (write-stream-start)
    ;; no encoding here
    #f)

  (define (write-stream-end)
    (flush-stream))

  (define (write-indicator indicator need-whitespace
                           [write-whitespace #f] [write-indention #f])
    (let ([data (if (or whitespace (not need-whitespace))
                    indicator
                    (format " ~a" indicator))])
      (set! whitespace write-whitespace)
      (set! indention (and indention write-indention))
      (set! column (+ column (string-length data)))
      (set! open-ended #f)
      (fprintf out data)))

  (define (write-indent)
    (let ([indent (or indent 0)])
      (when (or (not indention) (> column indent)
                (and (= column indent) (not whitespace)))
        (write-line-break))
      (when (< column indent)
        (let ([n (- indent column)])
          (set! whitespace #t)
          (set! column indent)
          (fprintf out (list->string (build-list n (λ _ #\space))))))))

  (define (write-line-break [data #f])
    (when (char? data)
      (set! data (string data)))
    (unless data
      (set! data best-line-break))
    (set! whitespace #t)
    (set! indention #t)
    (set! line (add1 line))
    (set! column 0)
    (fprintf out data))

  (define (write-version-directive version-text)
    (fprintf out (format "%YAML ~a" version-text))
    (write-line-break))

  (define (write-tag-directive handle-text prefix-text)
    (fprintf out (format "%TAG ~a ~a" handle-text prefix-text))
    (write-line-break))

  ;; Scalar streams.

  (define (write-single-quoted text [split #t])
    (write-indicator "'" #t)
    (let ([spaces #f]
          [breaks #f]
          [start 0]
          [end 0])
      (while (<= end (string-length text))
        (let ([ch #f])
          (when (< end (string-length text))
            (set! ch (string-ref text end)))
          (cond
           [spaces
            (unless (char=? #\space ch)
              (if (and (= (+ 1 start) end)
                       (> column best-width)
                       split
                       (not (zero? start))
                       (not (= end (string-length text))))
                  (write-indent)
                  (let ([data (substring text start end)])
                    (set! column (+ column (string-length data)))
                    (fprintf out data)))
              (set! start end))]
           [breaks
            (unless (and (char? ch) (string-index "\n\x85\u2028\u2029" ch))
              (when (char=? (string-ref text start) #\newline)
                (write-line-break))
              (for ([br (substring text start end)])
                (if (char=? br #\newline)
                    (write-line-break)
                    (write-line-break br)))
              (write-indent)
              (set! start end))]
           [else
            (when (or (not (char? ch))
                      (string-index " \n\x85\u2028\u2029" ch)
                      (char=? #\' ch))
              (when (< start end)
                (let ([data (substring text start end)])
                  (set! column (+ column (string-length data)))
                  (fprintf out data)
                  (set! start end))))])
          (when (equal? #\' ch)
            (set! column (+ column 2))
            (fprintf out "''")
            (set! start (add1 end)))
          (when (char? ch)
            (set! spaces (char=? #\space ch))
            (set! breaks (string-index "\n\x85\u2028\u2029" ch)))
          (set! end (add1 end))))
      (write-indicator "'" #f)))

  (define (write-double-quoted text [split #t])
    (define ESCAPE-REPLACEMENTS
      #hash((#\nul . #\0)
            (#\u0007 . #\a)
            (#\backspace . #\b)
            (#\tab . #\t)
            (#\newline . #\n)
            (#\vtab . #\v)
            (#\page . #\f)
            (#\return . #\r)
            (#\u001B . #\e)
            (#\" . #\")
            (#\\ . #\\)
            (#\u0085 . #\N)
            (#\u00A0 . #\_)
            (#\u2028 . #\L)
            (#\u2029 . #\P)))
    (let ([start 0] [end 0])
      (write-indicator "\"" #t)
      (while (<= end (string-length text))
        (let ([ch #f])
          (when (< end (string-length text))
            (set! ch (string-ref text end)))
          (when (or (not (char? ch))
                    (string-index "\"\\\x85\u2028\u2029\uFEFF" ch)
                    (not (char<=? #\space ch #\~))
                    (and allow-unicode
                         (or (char<=? #\u00A0 ch #\uD7FF)
                             (char<=? #\uE000 ch #\uFFFD))))
            (when (< start end)
              (let ([data (substring text start end)])
                (set! column (+ column (string-length data)))
                (fprintf out data)
                (set! start end)))
            (when (char? ch)
              (let ([data ""])
                (cond
                 [(hash-has-key? ESCAPE-REPLACEMENTS ch)
                  (let ([esc (hash-ref ESCAPE-REPLACEMENTS ch)])
                    (set! data (format "\\~a" esc)))]
                 [(char<=? ch #\u00FF)
                  (let ([hex (number->string (char->integer ch) 16)])
                    (set! data (format "\\x~a" (string-upcase hex))))]
                 [(char<=? ch #\uFFFF)
                  (let ([hex (number->string (char->integer ch) 16)])
                    (if (= 2 (string-length hex))
                        (set! data (format "\\u00~a" (string-upcase hex)))
                        (set! data (format "\\u~a" (string-upcase hex)))))]
                 [else
                  (let ([hex (number->string (char->integer ch) 16)])
                    (if (= 2 (string-length hex))
                        (set! data (format "\\U000000~a" (string-upcase hex)))
                        (set! data (format "\\U0000~a" (string-upcase hex)))))])
                (set! column (+ column (string-length data)))
                (fprintf out data)
                (set! start (add1 end)))))
          (when (and (< 0 end (sub1 (string-length text)))
                     (or (equal? #\space ch) (>= start end))
                     (> (+ column (- end start)) best-width)
                     split)
            (let ([data (string-append (substring text start end) "\\")])
              (when (< start end)
                (set! start end))
              (set! column (+ column (string-length data)))
              (fprintf out data)
              (write-indent)
              (set! whitespace #f)
              (set! indention #f)
              (when (char=? #\space (string-ref text start))
                (let ([data "\\"])
                  (set! column (+ column (string-length data)))
                  (fprintf out data)))))
          (set! end (add1 end))))
      (write-indicator "\"" #f)))

  (define (determine-block-hints text)
    (let ([hints ""])
      (when (and (string? text) (> (string-length text) 0))
        (when (string-index " \n\x85\u2028\u2029" (string-ref text 0))
          (set! hints (format "~a~a" hints best-indent)))
        (cond
         [(not (string-index "\n\x85\u2028\u2029"
                             (string-ref text (- (string-length text) 1))))
          (set! hints (string-append hints "-"))]
         [(or (= 1 (string-length text))
              (string-index "\n\x85\u2028\u2029"
                            (string-ref text (- (string-length text) 2))))
          (set! hints (string-append hints "+"))]))
      hints))

  (define (write-folded text)
    (let ([hints (determine-block-hints text)]
          [leading-space #t]
          [spaces #f]
          [breaks #t]
          [start 0]
          [end 0])
      (write-indicator (string-append ">" hints) #t)
      (when (and (> (string-length hints) 0)
                 (char=? #\+ (string-ref hints
                                         (sub1 (string-length hints)))))
        (set! open-ended #t))
      (write-line-break)
      (while (<= end (string-length text))
        (let ([ch #f])
          (when (< end (string-length text))
            (set! ch (string-ref text end)))
          (cond
           [breaks
            (unless (and (char? ch)
                         (string-index "\n\x85\u2028\u2029" ch))
              (when (and (not leading-space)
                         (char? ch)
                         (not (char=? #\space ch))
                         (char=? #\newline (string-ref text start)))
                (write-line-break))
              (set! leading-space (equal? #\space ch))
              (for ([br (substring text start end)])
                (if (char=? #\newline br)
                    (write-line-break)
                    (write-line-break br)))
              (when (char? ch)
                (write-indent))
              (set! start end))]
           [spaces
            (unless (and (char? ch)
                         (char=? #\space ch))
              (if (and (= (add1 start) end) (> column best-width))
                  (write-indent)
                  (let ([data (substring text start end)])
                    (set! column (+ column (string-length data)))
                    (fprintf out data)))
              (set! start end))]
           [else
            (when (or (not (char? ch))
                      (string-index "\n\x85\u2028\u2029" ch))
              (let ([data (substring text start end)])
                (set! column (+ column (string-length data)))
                (fprintf out data)
                (unless (char? ch)
                  (write-line-break))
                (set! start end)))])
          (when (char? ch)
            (set! breaks (string-index "\n\x85\u2028\u2029" ch))
            (set! spaces (char=? #\space ch)))
          (set! end (add1 end))))))

  (define (write-literal text)
    (let ([hints (determine-block-hints text)]
          [breaks #t]
          [start 0]
          [end 0])
      (write-indicator (string-append "|" hints) #t)
      (when (and (> (string-length hints) 0)
                 (char=? #\+ (string-ref hints
                                         (sub1 (string-length hints)))))
        (set! open-ended #t))
      (write-line-break)
      (while (<= end (string-length text))
        (let ([ch #f])
          (when (< end (string-length text))
            (set! ch (string-ref text end)))
          (when breaks
             (when (or (not (char? ch))
                       (not (string-index "\n\x85\u2028\u2029" ch)))
               (for ([br (substring text start end)])
                 (if (char=? #\newline br)
                     (write-line-break)
                     (write-line-break br)))
               (when (char? ch)
                 (write-indent))
               (set! start end)))
          (unless breaks
            (when (or (not (char? ch))
                      (string-index "\n\x85\u2028\u2029" ch))
              (fprintf out (substring text start end))
              (unless (char? ch)
                (write-line-break))
              (set! start end)))
          (when (char? ch)
            (set! breaks (string-index "\n\x85\u2028\u2029" ch)))
          (set! end (add1 end))))))

  (define (write-plain text [split #t])
    (when root-context
      (set! open-ended #t))
    (when (> (string-length text) 0)
      (let ([spaces #f]
            [breaks #f]
            [start 0]
            [end 0])
        (unless whitespace
          (let ([data " "])
            (set! column (+ column (string-length data)))
            (fprintf out data)))
        (set! whitespace #f)
        (set! indention #f)
        (while (<= end (string-length text))
          (let ([ch #f])
            (when (< end (string-length text))
              (set! ch (string-ref text end)))
            (cond
             [spaces
              (unless (equal? #\space ch)
                (cond
                 [(and (= (add1 start) end)
                       (> column best-width)
                       split)
                  (write-indent)
                  (set! whitespace #f)
                  (set! indention #f)]
                 [else
                  (let ([data (substring text start end)])
                    (set! column (+ column (string-length data)))
                    (fprintf out data))])
                (set! start end))]
             [breaks
              (unless (and (char? ch)
                           (string-index "\n\x85\u2028\u2029" ch))
                (when (char=? #\newline (string-ref text start))
                  (write-line-break))
                (for ([br (substring text start end)])
                  (if (char=? #\newline br)
                      (write-line-break)
                      (write-line-break br)))
                (write-indent)
                (set! whitespace #f)
                (set! indention #f)
                (set! start end))]
             [else
              (unless (and (char? ch)
                           (string-index "\n\x85\u2028\u2029" ch))
                (let ([data (substring text start end)])
                  (set! column (+ column (string-length data)))
                  (fprintf out data)
                  (set! start end)))])
            (when (char? ch)
              (set! spaces (char=? #\space ch))
              (set! breaks (string-index "\n\x85\u2028\u2029" ch)))
            (set! end (add1 end)))))))

  (values emit))

(module+ test
  (require rackunit racket/generator "parser.rkt")
  (define-simple-check (check-emitter test-file check-file)
    (let* ([out (open-output-string)]
           [in (open-input-file check-file)]
           [emit (make-emitter out)])
      (for ([event (parse-file test-file)])
        (emit event))
      (check-equal? (get-output-string out) (port->string in))
      (close-output-port out)
      (close-input-port in)))
  (test-begin
   (for ([(test-file check-file) (test-files #"emit")])
     (check-emitter test-file check-file))))