# Rich \`text%\` editors

**URL:** <https://racket.discourse.group/t/rich-text-editors/1267>\
**Category:** Questions & Answers\
**Tags:** question, gui\
**Created:** [August 31, 2022, 8:27pm UTC](https://racket.discourse.group/t/rich-text-editors/1267 "2022-08-31T20:27:58Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![LiberalArtist](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/liberalartist/32/151_2.png) [@LiberalArtist](https://racket.discourse.group/u/LiberalArtist)\
**Post date:** [August 31, 2022, 8:27pm UTC](https://racket.discourse.group/t/rich-text-editors/1267/1 "2022-08-31T20:27:58Z")

</div>

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:

```scheme
(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%](https://alex-hhh.github.io/2020/05/markdown-view.html), I know how to display such an `xexpr/c` in a `text%`:

```scheme
#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)

```

 ![Screenshot of frame f.](https://global.discourse-cdn.com/free1/uploads/racket/original/1X/fb2bf0883e53d318b01c727be8404bcb711ae639.png)

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.

---

<div class="post-metadata">

**Author:** ![jjsimpso](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/jjsimpso/32/602_2.png) [@jjsimpso](https://racket.discourse.group/u/jjsimpso)\
**Post date:** [September 1, 2022, 3:21am UTC](https://racket.discourse.group/t/rich-text-editors/1267/2 "2022-09-01T03:21:46Z")

</div>

I'd probably start with [5&nbsp;Editors](https://docs.racket-lang.org/gui/editor-overview.html), 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.

---

<div class="post-metadata">

**Author:** ![alexh](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/alexh/32/315_2.png) [@alexh](https://racket.discourse.group/u/alexh)\
**Post date:** [September 1, 2022, 11:36am UTC](https://racket.discourse.group/t/rich-text-editors/1267/3 "2022-09-01T11:36:23Z")

</div>

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.

```scheme
(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.
