Rich `text%` editors

I'm interested in using text% from racket/gui for an application that needs a rich text editor. I'm interested in general in any existing work along these lines, but I have a very basic question to start with: How can I get structured data from an editor with styled text?

To be concrete, imagine a tiny subset of HTML:

(flat-rec-contract xexpr/c
  string?
  (cons/c 'b (listof xexpr/c))
  (cons/c 'i (listof xexpr/c))
  (cons/c 'span (listof xexpr/c)))

Thanks to Markdown View using the Racket editor%, I know how to display such an xexpr/c in a text%:

#lang racket/gui

(define (insert-html ed x)
  (dynamic-wind
   (λ ()
     (send ed begin-edit-sequence))
   (λ ()
     (define s-l (send ed get-style-list))
     (define plain-style
       (send s-l
             find-named-style
             (send ed default-style-name)))
     (define bold-style
       (send s-l
             find-or-create-style
             plain-style
             (make-object style-delta% 'change-bold)))
     (define italic-style
       (send s-l
             find-or-create-style
             plain-style
             (make-object style-delta% 'change-style 'italic)))
     (define bold-italic-style
       (send s-l
             find-or-create-join-style
             bold-style
             italic-style))
     (let loop ([modes #hasheq()]
                [x x])
       (match x
         [(cons 'span xs)
          (for ([x (in-list xs)])
            (loop modes x))]
         [(cons 'b xs)
          (for ([x (in-list xs)])
            (loop (hash-set modes 'b #t) x))]
         [(cons 'i xs)
          (for ([x (in-list xs)])
            (loop (hash-set modes 'i #t) x))]
         [(? string?)
          (define start (send ed last-position))
          (send ed insert x)
          (define end (send ed last-position))
          (send ed
                change-style
                (match (hash-keys modes 'ordered)
                  ['(b i)
                   bold-italic-style]
                  ['(b)
                   bold-style]
                  ['(i)
                   italic-style]
                  ['()
                   plain-style])
                start
                end)])))
   (λ ()
     (send ed end-edit-sequence))))

(define txt
  (new text% [auto-wrap #t]))

(insert-html
 txt
 `(span "This is some text with "
        (b "bold " (i "and italic"))
        " parts. It also has "
        (i "italic " (b "and bold"))
        " parts."))

(define f
  (new frame%
       [label "Rich text%"]
       [width 400]
       [height 400]))

(define ec
  (new editor-canvas%
       [parent f]
       [style '(no-hscroll)]
       [editor txt]))

(send f show #t)

Similarly, I can imagine how to add buttons and menu items so the user can edit text and styling in the usual way.

What I don't know is how to get the contents of an editor like txt as an xexpr/c. I know there are various methods like find-first-snip and get-style in snip%, but I'm not clear on how change-style interacts with creating/splitting/joining snips, or if there's some better approach overall.

2 Likes

I'd probably start with 5 Editors, section 5.2. Editors do have an internal file format for saving and cut/paste, but I doubt it would be anything like xexpr/c. The insert-html code is parsing the structured text and transforming it into string snips, so you are losing the original structure/format.

If your format is really simple you might be able to reverse it by doing an inverse transformation on the snips' styles, but that doesn't seem ideal. Maybe someone else here knows more about the wxme format or snip-class%, which seems to be relevant here.

You can use the following code to get a xexpr/c back from the text% class. This will not be the same sexpr that you put in, but it will produce the same result if you feed it back to insert-html. Also, the code produces somewhat inefficient xexpr/c, but easy optimizations are possible.

(define (style-snip snip)
  (define style (send snip get-style))
  (define text (send snip get-text 0 (send snip get-count)))
  (define s0
    (if (equal? (send style get-weight) 'bold)
        (list 'b text)
        text))
  (define s1
    (if (equal? (send style get-style) 'italic)
        (list 'i s0)
        s0))
  s1)

(define (extract-sexpr txt)
  (let loop ([result '()]
             [snip (send txt find-first-snip)])
    (if snip
        (if (is-a? snip string-snip%)
            (loop (cons (style-snip snip) result)
                  (send snip next))
            (loop result
                  (send snip next)))
        (cons 'span (reverse result)))))

Of course, you probably want to extract something that resembles the original xexpr/c, but in that case, you'll need to maintain your own "document object model" and update it from the text% instance by overriding the various "after-insert", "after-update" etc.
methods. Depending how "simple" you want the end result to be, this can end up being very complex.

Alex.

3 Likes