Skip to main content

ui/
rich_doc.rs

1// rich_doc -- the document of ui.richtext: paragraphs of formatted runs, the selection and the
2// undo history (ui::History); JSON, HTML (rich_html.rs) and plain text
3//
4//   RichDoc::new() / from_plain(s) / from_paras(v) / from_json(s) / from_html(s)
5//   doc.to_json() / to_html() / to_plain() (list markers) / text() (paragraphs joined by '\n')
6//   doc.paras() / para(i) / selection() / set_selection(sel) / revision()
7//   editing at the selection (each one undoable step): insert_text(s), insert_paras(frag)
8//   (paste), delete(a, b), split() (Enter), toggle(RF_*), set_color / set_bg / set_size /
9//   set_font / set_link / clear_format, set_align(RA_*), toggle_list(LS_*), indent(delta),
10//   set_style(PS_*); with an empty selection the character formats apply to the text typed
11//   next (current_fmt shows them)
12//   doc.undo() / redo() / can_undo() / can_redo() / undo_label() / is_dirty() / mark_saved()
13//
14//   Fmt { flags RF_BOLD | RF_ITALIC | RF_UNDER | RF_STRIKE, color, bg, size (1/10 pt),
15//         font (family), link }      0 / "" = the editor's default
16//   Para { align RA_*, list LS_*, level 0..8, style PS_* } + its text and runs
17//
18// JSON ("jfrich" 1):
19//   {"jfrich":1,"font":"","size":11,"paras":[{"align":"center","list":"bullet","level":1,
20//    "style":"h1","runs":[{"t":"Hallo ","b":1},{"t":"Welt","i":1,"u":1,"s":1,"color":"#c00000",
21//    "bg":"#ffff00","size":14.5,"font":"DejaVu Serif","link":"https://example.org"}],
22//    "mark":{"b":1}}]}
23// (attributes at their default are left out; "mark" is the format of an empty paragraph).
24//
25// Positions are TextPos { line: paragraph, byte: UTF-8 offset into the paragraph's text };
26// a paragraph holds no '\n'.  An edit replaces a range of whole paragraphs (ParaEdit: old and
27// new paragraphs, the selection before and after), so undo restores text, formats and
28// paragraph attributes alike; typing and repeated Backspace / Delete merge into one step.
29
30use alloc::boxed::Box;
31use alloc::format;
32use alloc::string::String;
33use alloc::vec::Vec;
34use core::sync::atomic::{AtomicU32, Ordering};
35
36use agent_core::json::{self, Limits, Value};
37
38use crate::ed_core::{TextPos, TextSel};
39use crate::history::{Edit, History};
40use crate::i18n::tr;
41
42/// Fmt::flags: bold
43pub const RF_BOLD: u8 = 1;
44/// Fmt::flags: italic
45pub const RF_ITALIC: u8 = 2;
46/// Fmt::flags: underlined
47pub const RF_UNDER: u8 = 4;
48/// Fmt::flags: struck through
49pub const RF_STRIKE: u8 = 8;
50/// every flag
51pub const RF_ALL: u8 = 15;
52
53/// Para::align: start (left)
54pub const RA_LEFT: u8 = 0;
55/// Para::align: centred
56pub const RA_CENTER: u8 = 1;
57/// Para::align: end (right)
58pub const RA_RIGHT: u8 = 2;
59/// both edges (the last row of a paragraph stays left)
60pub const RA_JUSTIFY: u8 = 3;
61
62/// Para::list: no list
63pub const LS_NONE: u8 = 0;
64/// a bulleted list item (•, ◦, ▪ by level)
65pub const LS_BULLET: u8 = 1;
66/// a numbered list item (1., a., i. by level)
67pub const LS_NUMBER: u8 = 2;
68
69/// Para::style: body text
70pub const PS_NORMAL: u8 = 0;
71/// headings (bold, larger)
72pub const PS_H1: u8 = 1;
73/// Para::style: heading 2
74pub const PS_H2: u8 = 2;
75/// Para::style: heading 3
76pub const PS_H3: u8 = 3;
77/// a quotation (indented, a bar on the left, the secondary text colour)
78pub const PS_QUOTE: u8 = 4;
79
80/// the deepest Para::level
81pub const LEVEL_MAX: u8 = 8;
82
83/// the format of a run of characters; zero / empty fields take the editor's defaults
84#[derive(Clone, Debug, Default, PartialEq, Eq)]
85pub struct Fmt {
86    /// RF_BOLD | RF_ITALIC | RF_UNDER | RF_STRIKE
87    pub flags: u8,
88    /// text colour 0xAARRGGBB (0: the theme's text colour)
89    pub color: u32,
90    /// highlight colour behind the text (0: none)
91    pub bg: u32,
92    /// size in tenths of a point (0: the document's size)
93    pub size: u16,
94    /// font family ("": the document's font)
95    pub font: String,
96    /// link target ("": not a link)
97    pub link: String,
98}
99
100impl Fmt {
101    /// the default format with these flags
102    pub fn flags(flags: u8) -> Fmt {
103        Fmt { flags, ..Fmt::default() }
104    }
105    /// bold
106    pub fn bold() -> Fmt {
107        Fmt::flags(RF_BOLD)
108    }
109    /// italic
110    pub fn italic() -> Fmt {
111        Fmt::flags(RF_ITALIC)
112    }
113    /// a link to url
114    pub fn link(url: &str) -> Fmt {
115        Fmt { link: String::from(url), ..Fmt::default() }
116    }
117}
118
119static NEXT_VER: AtomicU32 = AtomicU32::new(1);
120
121fn next_ver() -> u32 {
122    NEXT_VER.fetch_add(1, Ordering::Relaxed)
123}
124
125/// a paragraph: its text (no '\n') with runs of formats, and its attributes.  The attribute
126/// fields may be set directly while building a document (not undoable; RichDoc's setters
127/// are).
128#[derive(Clone, Debug)]
129pub struct Para {
130    text: String,
131    // (end, format) ascending; the last ends at text.len(); empty for an empty paragraph
132    runs: Vec<(usize, Fmt)>,
133    /// the format of the paragraph's end: typing into the empty paragraph takes it
134    pub mark: Fmt,
135    /// RA_*
136    pub align: u8,
137    /// LS_*
138    pub list: u8,
139    /// indent / list level 0..LEVEL_MAX
140    pub level: u8,
141    /// PS_*
142    pub style: u8,
143    // a new number with every change (layout caches)
144    ver: u32,
145}
146
147impl PartialEq for Para {
148    fn eq(&self, o: &Para) -> bool {
149        self.text == o.text
150            && self.runs == o.runs
151            && self.mark == o.mark
152            && self.align == o.align
153            && self.list == o.list
154            && self.level == o.level
155            && self.style == o.style
156    }
157}
158
159impl Default for Para {
160    fn default() -> Para {
161        Para::new()
162    }
163}
164
165impl Para {
166    /// an empty body paragraph
167    pub fn new() -> Para {
168        Para { text: String::new(), runs: Vec::new(), mark: Fmt::default(), align: RA_LEFT, list: LS_NONE, level: 0, style: PS_NORMAL, ver: next_ver() }
169    }
170
171    /// a paragraph of unformatted text ('\n' and '\r' are dropped)
172    pub fn plain(text: &str) -> Para {
173        let mut p = Para::new();
174        p.push(text, &Fmt::default());
175        p
176    }
177
178    /// builder: text in a format appended
179    pub fn with(mut self, text: &str, fmt: Fmt) -> Para {
180        self.push(text, &fmt);
181        self
182    }
183
184    /// builder: the attributes
185    pub fn attrs(mut self, align: u8, list: u8, level: u8, style: u8) -> Para {
186        self.align = align.min(RA_JUSTIFY);
187        self.list = list.min(LS_NUMBER);
188        self.level = level.min(LEVEL_MAX);
189        self.style = style.min(PS_QUOTE);
190        self
191    }
192
193    /// append text in a format ('\n' and '\r' are dropped)
194    pub fn push(&mut self, text: &str, fmt: &Fmt) {
195        let clean: String;
196        let text = if text.contains(['\n', '\r']) {
197            clean = text.chars().filter(|&c| c != '\n' && c != '\r').collect();
198            &clean
199        } else {
200            text
201        };
202        if text.is_empty() {
203            return;
204        }
205        self.text.push_str(text);
206        let end = self.text.len();
207        match self.runs.last_mut() {
208            Some(r) if r.1 == *fmt => r.0 = end,
209            _ => self.runs.push((end, fmt.clone())),
210        }
211        self.ver = next_ver();
212    }
213
214    /// the paragraph's text (no '\n')
215    pub fn text(&self) -> &str {
216        &self.text
217    }
218
219    /// the text's length in bytes
220    pub fn len(&self) -> usize {
221        self.text.len()
222    }
223
224    /// no text
225    pub fn is_empty(&self) -> bool {
226        self.text.is_empty()
227    }
228
229    /// the runs: (text, format)
230    pub fn runs(&self) -> impl Iterator<Item = (&str, &Fmt)> + '_ {
231        let mut start = 0;
232        self.runs.iter().map(move |(end, f)| {
233            let s = &self.text[start..*end];
234            start = *end;
235            (s, f)
236        })
237    }
238
239    /// the runs as (start, end, format)
240    pub fn spans(&self) -> impl Iterator<Item = (usize, usize, &Fmt)> + '_ {
241        let mut start = 0;
242        self.runs.iter().map(move |(end, f)| {
243            let s = start;
244            start = *end;
245            (s, *end, f)
246        })
247    }
248
249    /// changes with every edit of the paragraph (layout caches)
250    pub fn version(&self) -> u32 {
251        self.ver
252    }
253
254    /// the format of the character at byte (the last one at the end; the mark when empty)
255    pub fn fmt_at(&self, byte: usize) -> &Fmt {
256        for (end, f) in &self.runs {
257            if byte < *end {
258                return f;
259            }
260        }
261        self.runs.last().map_or(&self.mark, |r| &r.1)
262    }
263
264    /// the format typing at byte takes: the character before it (at the start: the first
265    /// character; empty: the mark); a link ends at its end
266    pub fn typing_fmt(&self, byte: usize) -> Fmt {
267        if self.text.is_empty() {
268            return self.mark.clone();
269        }
270        let mut f = if byte == 0 { self.fmt_at(0).clone() } else { self.fmt_at(byte - 1).clone() };
271        if !f.link.is_empty() && (byte == 0 || byte >= self.text.len() || self.fmt_at(byte).link != f.link) {
272            f.link.clear();
273        }
274        f
275    }
276
277    // the part a..b with this paragraph's attributes (mark: the format at b)
278    fn slice(&self, a: usize, b: usize) -> Para {
279        let mut p = Para {
280            text: String::new(),
281            runs: Vec::new(),
282            mark: self.mark.clone(),
283            align: self.align,
284            list: self.list,
285            level: self.level,
286            style: self.style,
287            ver: 0,
288        };
289        for (s, e, f) in self.spans() {
290            let (s, e) = (s.max(a), e.min(b));
291            if s < e {
292                p.push(&self.text[s..e], f);
293            }
294        }
295        if b < self.text.len() || self.text.is_empty() {
296            p.mark = if self.text.is_empty() { self.mark.clone() } else { self.typing_fmt(b) };
297        }
298        p.ver = next_ver();
299        p
300    }
301
302    // append another paragraph's text and runs
303    fn append(&mut self, o: &Para) {
304        for (s, f) in o.runs() {
305            self.push(s, f);
306        }
307    }
308
309    // change the formats of a..b
310    fn map_fmt(&mut self, a: usize, b: usize, f: &dyn Fn(&mut Fmt)) {
311        if a >= b {
312            return;
313        }
314        let old = core::mem::take(&mut self.runs);
315        let text = core::mem::take(&mut self.text);
316        let mut start = 0;
317        for (end, fmt) in old {
318            for (s, e, inside) in [(start, end.min(a).max(start), false), (start.max(a), end.min(b), true), (b.max(start), end, false)] {
319                if s < e {
320                    let mut g = fmt.clone();
321                    if inside {
322                        f(&mut g);
323                    }
324                    self.push(&text[s..e], &g);
325                }
326            }
327            start = end;
328        }
329        self.ver = next_ver();
330    }
331
332    fn touch(&mut self) {
333        self.ver = next_ver();
334    }
335
336    // the same attributes as o
337    fn attrs_of(&mut self, o: &Para) {
338        self.align = o.align;
339        self.list = o.list;
340        self.level = o.level;
341        self.style = o.style;
342    }
343}
344
345// ------------------------------------------------------------------ the undoable state
346
347/// the content an edit changes: the paragraphs and the selection
348#[derive(Clone)]
349pub(crate) struct RichBody {
350    paras: Vec<Para>,
351    sel: TextSel,
352}
353
354// merge classes of ParaEdit
355const K_NONE: u8 = 0;
356const K_TYPE: u8 = 1;
357const K_BACK: u8 = 2;
358const K_DEL: u8 = 3;
359
360// paragraphs first .. first + old.len() become new
361struct ParaEdit {
362    first: usize,
363    old: Vec<Para>,
364    new: Vec<Para>,
365    sel0: TextSel,
366    sel1: TextSel,
367    label: String,
368    kind: u8,
369}
370
371impl Edit<RichBody> for ParaEdit {
372    fn apply(&mut self, s: &mut RichBody) {
373        s.paras.splice(self.first..self.first + self.old.len(), self.new.iter().cloned());
374        s.sel = self.sel1;
375    }
376    fn revert(&mut self, s: &mut RichBody) {
377        s.paras.splice(self.first..self.first + self.new.len(), self.old.iter().cloned());
378        s.sel = self.sel0;
379    }
380    fn label(&self) -> &str {
381        &self.label
382    }
383    fn merge(&mut self, next: &mut dyn Edit<RichBody>) -> bool {
384        if self.kind == K_NONE {
385            return false;
386        }
387        match next.value().and_then(|v| v.downcast_ref::<ParaEdit>()) {
388            Some(n) if n.kind == self.kind && n.first == self.first && n.sel0 == self.sel1 && n.old == self.new => {
389                self.new = n.new.clone();
390                self.sel1 = n.sel1;
391                true
392            }
393            _ => false,
394        }
395    }
396    fn value(&self) -> Option<&dyn core::any::Any> {
397        Some(self)
398    }
399}
400
401/// the document of ui.richtext (see the module header)
402pub struct RichDoc {
403    body: RichBody,
404    hist: History<RichBody>,
405    rev: u64,
406    // the format typed next at this caret (a format chosen without a selection)
407    typing: Option<(Fmt, TextPos)>,
408    // tells documents apart (editor caches)
409    uid: u32,
410    /// the document's font family ("": the UI font)
411    pub font: String,
412    /// the document's size in tenths of a point (0: 11 pt)
413    pub size: u16,
414}
415
416impl Default for RichDoc {
417    fn default() -> RichDoc {
418        RichDoc::new()
419    }
420}
421
422impl Clone for RichDoc {
423    /// the content and the selection (not the undo history)
424    fn clone(&self) -> RichDoc {
425        let mut d = RichDoc::from_paras(self.body.paras.clone());
426        d.body.sel = self.body.sel;
427        d.font = self.font.clone();
428        d.size = self.size;
429        d
430    }
431}
432
433const HIST_LIMIT: usize = 500;
434
435impl RichDoc {
436    /// one empty paragraph
437    pub fn new() -> RichDoc {
438        RichDoc::from_paras(Vec::new())
439    }
440
441    /// paragraphs (an empty list: one empty paragraph); the caret at the start
442    pub fn from_paras(mut paras: Vec<Para>) -> RichDoc {
443        if paras.is_empty() {
444            paras.push(Para::new());
445        }
446        RichDoc { body: RichBody { paras, sel: TextSel::default() }, hist: History::new(HIST_LIMIT), rev: 0, typing: None, uid: next_ver(), font: String::new(), size: 0 }
447    }
448
449    /// unformatted text: a paragraph per line
450    pub fn from_plain(s: &str) -> RichDoc {
451        RichDoc::from_paras(lines_of(s).map(Para::plain).collect())
452    }
453
454    /// the content (no history)
455    pub fn paras(&self) -> &[Para] {
456        &self.body.paras
457    }
458
459    /// paragraph i (clamped to the last one)
460    pub fn para(&self, i: usize) -> &Para {
461        &self.body.paras[i.min(self.body.paras.len() - 1)]
462    }
463
464    /// the number of paragraphs (at least 1)
465    pub fn para_count(&self) -> usize {
466        self.body.paras.len()
467    }
468
469    /// counts the changes of the content (edits, undo, redo; not the selection)
470    pub fn revision(&self) -> u64 {
471        self.rev
472    }
473
474    // (rich_edit.rs) a number of this document instance
475    pub(crate) fn uid(&self) -> u32 {
476        self.uid
477    }
478
479    /// the selection (the caret: anchor == caret)
480    pub fn selection(&self) -> TextSel {
481        self.body.sel
482    }
483
484    /// move the selection (clamped to the text, not undoable)
485    pub fn set_selection(&mut self, sel: TextSel) {
486        let sel = TextSel::new(self.clamp(sel.anchor), self.clamp(sel.caret));
487        self.body.sel = sel;
488    }
489
490    /// p inside the document, on a character boundary
491    pub fn clamp(&self, p: TextPos) -> TextPos {
492        let line = p.line.min(self.body.paras.len() - 1);
493        let t = &self.body.paras[line].text;
494        let mut b = p.byte.min(t.len());
495        while !t.is_char_boundary(b) {
496            b -= 1;
497        }
498        TextPos::new(line, b)
499    }
500
501    /// the end of the document
502    pub fn end(&self) -> TextPos {
503        let l = self.body.paras.len() - 1;
504        TextPos::new(l, self.body.paras[l].len())
505    }
506
507    /// the text: the paragraphs joined by '\n' (no list markers)
508    pub fn text(&self) -> String {
509        let mut s = String::new();
510        for (i, p) in self.body.paras.iter().enumerate() {
511            if i > 0 {
512                s.push('\n');
513            }
514            s.push_str(&p.text);
515        }
516        s
517    }
518
519    /// the byte offset of p in text()
520    pub fn flat(&self, p: TextPos) -> usize {
521        let p = self.clamp(p);
522        self.body.paras[..p.line].iter().map(|q| q.text.len() + 1).sum::<usize>() + p.byte
523    }
524
525    /// the position of byte offset f of text()
526    pub fn unflat(&self, f: usize) -> TextPos {
527        let mut f = f;
528        for (i, p) in self.body.paras.iter().enumerate() {
529            if f <= p.text.len() {
530                return self.clamp(TextPos::new(i, f));
531            }
532            f -= p.text.len() + 1;
533        }
534        self.end()
535    }
536
537    /// the text between a and b (paragraphs joined by '\n')
538    pub fn text_range(&self, a: TextPos, b: TextPos) -> String {
539        let (a, b) = (self.clamp(a.min(b)), self.clamp(a.max(b)));
540        let mut s = String::new();
541        for l in a.line..=b.line {
542            let t = &self.body.paras[l].text;
543            let s0 = if l == a.line { a.byte } else { 0 };
544            let s1 = if l == b.line { b.byte } else { t.len() };
545            if l > a.line {
546                s.push('\n');
547            }
548            s.push_str(&t[s0..s1]);
549        }
550        s
551    }
552
553    /// the paragraphs between a and b (the first and the last cut there), with attributes
554    pub fn paras_range(&self, a: TextPos, b: TextPos) -> Vec<Para> {
555        let (a, b) = (self.clamp(a.min(b)), self.clamp(a.max(b)));
556        (a.line..=b.line)
557            .map(|l| {
558                let p = &self.body.paras[l];
559                p.slice(if l == a.line { a.byte } else { 0 }, if l == b.line { b.byte } else { p.len() })
560            })
561            .collect()
562    }
563
564    /// the selected text
565    pub fn selected_text(&self) -> String {
566        let (a, b) = self.body.sel.range();
567        self.text_range(a, b)
568    }
569
570    /// the selected paragraphs (a fragment for the clipboard)
571    pub fn selected_paras(&self) -> Vec<Para> {
572        let (a, b) = self.body.sel.range();
573        self.paras_range(a, b)
574    }
575
576    /// the paragraph with the caret
577    pub fn current_para(&self) -> &Para {
578        &self.body.paras[self.body.sel.caret.line]
579    }
580
581    /// the format typing takes now (a format chosen without a selection, else the text's at
582    /// the caret); with a selection: its first character's, the flags all of it has
583    pub fn current_fmt(&self) -> Fmt {
584        let sel = self.body.sel;
585        if sel.is_empty() {
586            if let Some((f, at)) = &self.typing {
587                if *at == sel.caret {
588                    return f.clone();
589                }
590            }
591            return self.body.paras[sel.caret.line].typing_fmt(sel.caret.byte);
592        }
593        let (a, b) = sel.range();
594        let mut first: Option<Fmt> = None;
595        let mut flags = RF_ALL;
596        for l in a.line..=b.line {
597            let p = &self.body.paras[l];
598            let (s0, s1) = (if l == a.line { a.byte } else { 0 }, if l == b.line { b.byte } else { p.len() });
599            for (s, e, f) in p.spans() {
600                if s.max(s0) < e.min(s1) {
601                    flags &= f.flags;
602                    if first.is_none() {
603                        first = Some(f.clone());
604                    }
605                }
606            }
607        }
608        let mut f = first.unwrap_or_else(|| self.body.paras[a.line].typing_fmt(a.byte));
609        f.flags &= flags;
610        f
611    }
612
613    /// the link target at p (None: no link there)
614    pub fn link_at(&self, p: TextPos) -> Option<&str> {
615        let p = self.clamp(p);
616        let para = &self.body.paras[p.line];
617        if p.byte >= para.len() {
618            return None;
619        }
620        let l = &para.fmt_at(p.byte).link;
621        (!l.is_empty()).then_some(l.as_str())
622    }
623
624    // ------------------------------------------------------------------ history
625
626    /// there is a step to undo
627    pub fn can_undo(&self) -> bool {
628        self.hist.can_undo()
629    }
630    /// there is a step to redo
631    pub fn can_redo(&self) -> bool {
632        self.hist.can_redo()
633    }
634    /// the label of the step undo takes back ("Fett", "Schreiben" ...)
635    pub fn undo_label(&self) -> Option<&str> {
636        self.hist.undo_label()
637    }
638    /// the label of the step redo does again
639    pub fn redo_label(&self) -> Option<&str> {
640        self.hist.redo_label()
641    }
642    /// take the last step back -> false: nothing to undo
643    pub fn undo(&mut self) -> bool {
644        let r = self.hist.undo(&mut self.body);
645        if r {
646            self.rev += 1;
647            self.typing = None;
648        }
649        r
650    }
651    /// do the last undone step again -> false: nothing to redo
652    pub fn redo(&mut self) -> bool {
653        let r = self.hist.redo(&mut self.body);
654        if r {
655            self.rev += 1;
656            self.typing = None;
657        }
658        r
659    }
660    /// the content differs from the state of the last mark_saved
661    pub fn is_dirty(&self) -> bool {
662        self.hist.dirty()
663    }
664    /// the content is saved now (is_dirty false until the next change)
665    pub fn mark_saved(&mut self) {
666        self.hist.mark_saved();
667    }
668    /// forget the undo steps
669    pub fn clear_history(&mut self) {
670        self.hist.clear();
671    }
672    /// the next edits are one undo step (until group_end)
673    pub fn group_begin(&mut self, label: &str) {
674        self.hist.group_begin(label);
675    }
676    /// close the step of group_begin
677    pub fn group_end(&mut self) {
678        self.hist.group_end();
679    }
680
681    // record paragraphs first .. first + n becoming `new`, the selection then sel
682    fn edit(&mut self, first: usize, n: usize, mut new: Vec<Para>, sel: TextSel, label: &str, kind: u8) -> bool {
683        if new.is_empty() {
684            new.push(Para::new());
685        }
686        let old: Vec<Para> = self.body.paras[first..first + n].to_vec();
687        if old == new {
688            self.body.sel = sel;
689            return false;
690        }
691        let e = ParaEdit { first, old, new, sel0: self.body.sel, sel1: sel, label: String::from(label), kind };
692        self.hist.apply(&mut self.body, Box::new(e));
693        self.rev += 1;
694        self.typing = None;
695        true
696    }
697
698    // the paragraphs that replace a..b by frag: frag[0] continues a's paragraph, the last
699    // one takes what follows b.  attrs: frag's paragraphs keep their attributes (a paste of
700    // whole paragraphs), else they take the host's.  fill: the mark of empty results (text
701    // edits).  -> (new paragraphs, the position after the inserted text)
702    fn spliced(&self, a: TextPos, b: TextPos, frag: &[Para], attrs: bool, fill: Option<&Fmt>) -> (Vec<Para>, TextPos) {
703        let host = &self.body.paras[a.line];
704        let last = &self.body.paras[b.line];
705        let k = frag.len().max(1) - 1;
706        let empty = Para::new();
707        let f = |i: usize| frag.get(i).unwrap_or(&empty);
708        let mut out: Vec<Para> = Vec::with_capacity(k + 1);
709        let mut p0 = host.slice(0, a.byte);
710        p0.append(f(0));
711        if attrs && k > 0 && a.byte == 0 {
712            p0.attrs_of(f(0));
713        }
714        p0.mark = if k == 0 { last.mark.clone() } else { f(0).mark.clone() };
715        let end;
716        if k == 0 {
717            end = TextPos::new(a.line, p0.len());
718            p0.append(&last.slice(b.byte, last.len()));
719            out.push(p0);
720        } else {
721            out.push(p0);
722            for i in 1..k {
723                let mut p = f(i).clone();
724                if !attrs {
725                    p.attrs_of(host);
726                }
727                p.touch();
728                out.push(p);
729            }
730            let mut pk = f(k).clone();
731            if !attrs {
732                pk.attrs_of(host);
733            }
734            end = TextPos::new(a.line + k, pk.len());
735            let tail = last.slice(b.byte, last.len());
736            pk.append(&tail);
737            pk.mark = tail.mark;
738            pk.touch();
739            out.push(pk);
740        }
741        if let Some(fm) = fill {
742            for p in out.iter_mut().filter(|p| p.is_empty()) {
743                p.mark = fm.clone();
744            }
745        }
746        (out, end)
747    }
748
749    // ------------------------------------------------------------------ text edits
750
751    /// replace the selection by text (a paragraph per line) in the current format
752    pub fn insert_text(&mut self, s: &str) -> bool {
753        self.insert_text_kind(s, if s.contains(['\n', '\r']) { K_NONE } else { K_TYPE })
754    }
755
756    // (rich_edit.rs) typing merges into one step; a paste does not
757    pub(crate) fn insert_text_kind(&mut self, s: &str, kind: u8) -> bool {
758        let sel = self.body.sel;
759        let fmt = match &self.typing {
760            Some((f, at)) if *at == sel.caret => f.clone(),
761            _ if sel.is_empty() => self.current_fmt(),
762            _ => {
763                // typing over a selection takes the format of its first character
764                let a = sel.start();
765                let p = &self.body.paras[a.line];
766                if a.byte < p.len() { p.fmt_at(a.byte).clone() } else { p.typing_fmt(a.byte) }
767            }
768        };
769        let frag: Vec<Para> = lines_of(s)
770            .map(|l| {
771                let mut p = Para::new();
772                p.push(l, &fmt);
773                p
774            })
775            .collect();
776        let (a, b) = self.body.sel.range();
777        let (new, end) = self.spliced(a, b, &frag, false, Some(&fmt));
778        let label = if kind == K_TYPE { tr("Schreiben") } else { tr("Einfügen") };
779        self.edit(a.line, b.line - a.line + 1, new, TextSel::at(end), label, kind)
780    }
781
782    /// replace the selection by paragraphs (a paste): several keep their attributes, one
783    /// is inserted into the paragraph at the caret
784    pub fn insert_paras(&mut self, frag: &[Para]) -> bool {
785        if frag.is_empty() {
786            return self.delete_selection();
787        }
788        let (a, b) = self.body.sel.range();
789        let (new, end) = self.spliced(a, b, frag, frag.len() > 1, None);
790        self.edit(a.line, b.line - a.line + 1, new, TextSel::at(end), tr("Einfügen"), K_NONE)
791    }
792
793    /// remove the selection
794    pub fn delete_selection(&mut self) -> bool {
795        let (a, b) = self.body.sel.range();
796        self.delete(a, b)
797    }
798
799    /// remove a..b (the caret goes to a)
800    pub fn delete(&mut self, a: TextPos, b: TextPos) -> bool {
801        self.delete_kind(a, b, K_NONE)
802    }
803
804    // (rich_edit.rs) Backspace / Delete merge with the previous one
805    pub(crate) fn delete_kind(&mut self, a: TextPos, b: TextPos, kind: u8) -> bool {
806        let (a, b) = (self.clamp(a.min(b)), self.clamp(a.max(b)));
807        if a == b {
808            return false;
809        }
810        let fill = self.body.paras[a.line].typing_fmt(a.byte);
811        let (new, end) = self.spliced(a, b, &[], false, Some(&fill));
812        // joining into an empty paragraph keeps the attributes of the one with text
813        let mut new = new;
814        if a.byte == 0 && self.body.paras[a.line].is_empty() && b.line > a.line {
815            let src = &self.body.paras[b.line];
816            new[0].attrs_of(src);
817        }
818        self.edit(a.line, b.line - a.line + 1, new, TextSel::at(end), tr("Löschen"), kind)
819    }
820
821    /// Enter: split the paragraph at the caret (the selection is replaced).  In an empty
822    /// list item the list ends instead (a nested one moves out a level); after a heading the
823    /// new paragraph is body text.
824    pub fn split(&mut self) -> bool {
825        let (a, b) = self.body.sel.range();
826        let host = &self.body.paras[a.line];
827        if a == b && host.is_empty() && host.list != LS_NONE {
828            let mut p = host.clone();
829            if p.level > 0 {
830                p.level -= 1;
831            } else {
832                p.list = LS_NONE;
833            }
834            p.touch();
835            return self.edit(a.line, 1, alloc::vec![p], self.body.sel, tr("Liste"), K_NONE);
836        }
837        let fmt = self.current_fmt();
838        let mut e1 = Para::new();
839        e1.mark = fmt.clone();
840        let mut e2 = Para::new();
841        e2.mark = fmt.clone();
842        let at_end = b.byte >= self.body.paras[b.line].len();
843        let heading = host.style != PS_NORMAL && host.style != PS_QUOTE;
844        let (mut new, end) = self.spliced(a, b, &[e1, e2], false, Some(&fmt));
845        if heading && at_end {
846            if let Some(p) = new.last_mut() {
847                p.style = PS_NORMAL;
848            }
849        }
850        self.edit(a.line, b.line - a.line + 1, new, TextSel::at(end), tr("Absatz"), K_NONE)
851    }
852
853    // the range a format applies to: the selection, or (link edits) the link at the caret
854    fn fmt_range(&self, link_word: bool) -> Option<(TextPos, TextPos)> {
855        let sel = self.body.sel;
856        if !sel.is_empty() {
857            return Some(sel.range());
858        }
859        if link_word {
860            let p = self.body.paras.get(sel.caret.line)?;
861            let at = sel.caret.byte;
862            let link = if at < p.len() && !p.fmt_at(at).link.is_empty() {
863                p.fmt_at(at).link.clone()
864            } else if at > 0 && !p.fmt_at(at - 1).link.is_empty() {
865                p.fmt_at(at - 1).link.clone()
866            } else {
867                return None;
868            };
869            // the run of characters with that link around the caret
870            let spans: Vec<(usize, usize, bool)> = p.spans().map(|(s, e, f)| (s, e, f.link == link)).collect();
871            let i = spans.iter().position(|&(s, e, m)| m && s <= at && (at < e || (at == e && at > 0)))?;
872            let mut s = spans[i].0;
873            let mut e = spans[i].1;
874            let mut j = i;
875            while j > 0 && spans[j - 1].2 {
876                j -= 1;
877                s = spans[j].0;
878            }
879            let mut j = i;
880            while j + 1 < spans.len() && spans[j + 1].2 {
881                j += 1;
882                e = spans[j].1;
883            }
884            return Some((TextPos::new(sel.caret.line, s), TextPos::new(sel.caret.line, e)));
885        }
886        None
887    }
888
889    // change the character formats of the selection (without one: the format typed next)
890    fn map_sel(&mut self, label: &str, link_word: bool, f: &dyn Fn(&mut Fmt)) -> bool {
891        let Some((a, b)) = self.fmt_range(link_word) else {
892            let mut t = self.current_fmt();
893            f(&mut t);
894            let caret = self.body.sel.caret;
895            // an empty paragraph keeps the format as its mark
896            if self.body.paras[caret.line].is_empty() {
897                let mut p = self.body.paras[caret.line].clone();
898                p.mark = t.clone();
899                p.touch();
900                let sel = self.body.sel;
901                self.edit(caret.line, 1, alloc::vec![p], sel, label, K_NONE);
902            }
903            self.typing = Some((t, caret));
904            return true;
905        };
906        let new: Vec<Para> = (a.line..=b.line)
907            .map(|l| {
908                let mut p = self.body.paras[l].clone();
909                let s0 = if l == a.line { a.byte } else { 0 };
910                let s1 = if l == b.line { b.byte } else { p.len() };
911                p.map_fmt(s0, s1, f);
912                if s1 >= p.len() || p.is_empty() {
913                    f(&mut p.mark);
914                }
915                p
916            })
917            .collect();
918        let sel = self.body.sel;
919        self.edit(a.line, b.line - a.line + 1, new, sel, label, K_NONE)
920    }
921
922    /// RF_BOLD / RF_ITALIC / RF_UNDER / RF_STRIKE on (when not all of the selection has it)
923    /// or off
924    pub fn toggle(&mut self, flag: u8) -> bool {
925        let on = self.current_fmt().flags & flag != flag;
926        let label = match flag {
927            RF_BOLD => tr("Fett"),
928            RF_ITALIC => tr("Kursiv"),
929            RF_UNDER => tr("Unterstrichen"),
930            _ => tr("Durchgestrichen"),
931        };
932        self.map_sel(label, false, &|f: &mut Fmt| if on { f.flags |= flag } else { f.flags &= !flag })
933    }
934
935    /// set (on) or clear flags
936    pub fn set_flags(&mut self, flags: u8, on: bool) -> bool {
937        self.map_sel(tr("Format"), false, &|f: &mut Fmt| if on { f.flags |= flags } else { f.flags &= !flags })
938    }
939
940    /// the text colour (0: the theme's)
941    pub fn set_color(&mut self, argb: u32) -> bool {
942        self.map_sel(tr("Textfarbe"), false, &|f: &mut Fmt| f.color = argb)
943    }
944
945    /// the highlight colour (0: none)
946    pub fn set_bg(&mut self, argb: u32) -> bool {
947        self.map_sel(tr("Hervorhebung"), false, &|f: &mut Fmt| f.bg = argb)
948    }
949
950    /// the size in tenths of a point (0: the document's)
951    pub fn set_size(&mut self, tenths: u16) -> bool {
952        let t = if tenths == 0 { 0 } else { tenths.clamp(10, 4000) };
953        self.map_sel(tr("Schriftgröße"), false, &|f: &mut Fmt| f.size = t)
954    }
955
956    /// the font family ("": the document's)
957    pub fn set_font(&mut self, family: &str) -> bool {
958        let fam = String::from(family);
959        self.map_sel(tr("Schriftart"), false, &|f: &mut Fmt| f.font = fam.clone())
960    }
961
962    /// a link on the selection (without one: on the link at the caret); "" removes it.  A
963    /// collapsed caret outside a link inserts the url as linked text.
964    pub fn set_link(&mut self, url: &str) -> bool {
965        let u = String::from(url);
966        if self.body.sel.is_empty() && self.fmt_range(true).is_none() {
967            if url.is_empty() {
968                return false;
969            }
970            let mut f = self.current_fmt();
971            f.link = u.clone();
972            self.typing = Some((f, self.body.sel.caret));
973            return self.insert_text_kind(url, K_NONE);
974        }
975        self.map_sel(tr("Link"), true, &|f: &mut Fmt| f.link = u.clone())
976    }
977
978    /// back to the default format (links stay)
979    pub fn clear_format(&mut self) -> bool {
980        self.map_sel(tr("Format löschen"), false, &|f: &mut Fmt| *f = Fmt { link: core::mem::take(&mut f.link), ..Fmt::default() })
981    }
982
983    // ------------------------------------------------------------------ paragraph edits
984
985    // change the attributes of the selected paragraphs
986    fn map_paras(&mut self, label: &str, f: &dyn Fn(&mut Para)) -> bool {
987        let (a, b) = self.body.sel.range();
988        let new: Vec<Para> = (a.line..=b.line)
989            .map(|l| {
990                let mut p = self.body.paras[l].clone();
991                f(&mut p);
992                p.touch();
993                p
994            })
995            .collect();
996        let sel = self.body.sel;
997        self.edit(a.line, b.line - a.line + 1, new, sel, label, K_NONE)
998    }
999
1000    /// RA_* for the selected paragraphs
1001    pub fn set_align(&mut self, align: u8) -> bool {
1002        let a = align.min(RA_JUSTIFY);
1003        self.map_paras(tr("Ausrichtung"), &|p: &mut Para| p.align = a)
1004    }
1005
1006    /// LS_BULLET / LS_NUMBER: the selected paragraphs become such list items, or plain
1007    /// paragraphs when all of them already are
1008    pub fn toggle_list(&mut self, kind: u8) -> bool {
1009        let (a, b) = self.body.sel.range();
1010        let all = (a.line..=b.line).all(|l| self.body.paras[l].list == kind);
1011        let k = if all { LS_NONE } else { kind.min(LS_NUMBER) };
1012        self.map_paras(tr("Liste"), &|p: &mut Para| p.list = k)
1013    }
1014
1015    /// LS_* without toggling
1016    pub fn set_list(&mut self, kind: u8) -> bool {
1017        let k = kind.min(LS_NUMBER);
1018        self.map_paras(tr("Liste"), &|p: &mut Para| p.list = k)
1019    }
1020
1021    /// one level deeper (delta 1) or out (-1), 0..LEVEL_MAX
1022    pub fn indent(&mut self, delta: i32) -> bool {
1023        self.map_paras(tr("Einzug"), &|p: &mut Para| p.level = (p.level as i32).saturating_add(delta).clamp(0, LEVEL_MAX as i32) as u8)
1024    }
1025
1026    /// PS_* for the selected paragraphs
1027    pub fn set_style(&mut self, style: u8) -> bool {
1028        let s = style.min(PS_QUOTE);
1029        self.map_paras(tr("Formatvorlage"), &|p: &mut Para| p.style = s)
1030    }
1031
1032    /// replace the whole content (one undo step)
1033    pub fn set_paras(&mut self, paras: Vec<Para>) -> bool {
1034        let n = self.body.paras.len();
1035        self.edit(0, n, paras, TextSel::default(), tr("Ersetzen"), K_NONE)
1036    }
1037
1038    // ------------------------------------------------------------------ JSON
1039
1040    /// the document as JSON (see the module header)
1041    pub fn to_json(&self) -> String {
1042        let mut paras = Vec::new();
1043        for p in &self.body.paras {
1044            let mut o = Value::Object(Vec::new());
1045            if p.align != RA_LEFT {
1046                o.insert("align", Value::string(["left", "center", "right", "justify"][p.align as usize & 3]));
1047            }
1048            if p.list != LS_NONE {
1049                o.insert("list", Value::string(if p.list == LS_BULLET { "bullet" } else { "number" }));
1050            }
1051            if p.level != 0 {
1052                o.insert("level", Value::u64(p.level as u64));
1053            }
1054            if p.style != PS_NORMAL {
1055                o.insert("style", Value::string(STYLE_NAMES[p.style as usize % STYLE_NAMES.len()]));
1056            }
1057            let runs: Vec<Value> = p
1058                .runs()
1059                .map(|(t, f)| {
1060                    let mut r = fmt_json(f);
1061                    if let Value::Object(v) = &mut r {
1062                        v.insert(0, (String::from("t"), Value::string(t)));
1063                    }
1064                    r
1065                })
1066                .collect();
1067            o.insert("runs", Value::Array(runs));
1068            if p.mark != Fmt::default() && p.is_empty() {
1069                o.insert("mark", fmt_json(&p.mark));
1070            }
1071            paras.push(o);
1072        }
1073        let mut root = Value::object([("jfrich", Value::u64(1))]);
1074        if !self.font.is_empty() {
1075            root.insert("font", Value::string(self.font.clone()));
1076        }
1077        if self.size != 0 {
1078            root.insert("size", size_json(self.size));
1079        }
1080        root.insert("paras", Value::Array(paras));
1081        json::stringify_with_limits(&root, big_limits()).unwrap_or_default()
1082    }
1083
1084    /// a document from to_json's JSON (None: not such a document)
1085    pub fn from_json(s: &str) -> Option<RichDoc> {
1086        let v = json::parse_with_limits(s.as_bytes(), big_limits()).ok()?;
1087        v.get("jfrich")?.as_u64().filter(|&n| n == 1)?;
1088        let mut paras = Vec::new();
1089        for pv in v.get("paras")?.as_array()? {
1090            let mut p = Para::new();
1091            let s = |k: &str| pv.get(k).and_then(|x| x.as_str());
1092            p.align = match s("align") {
1093                Some("center") => RA_CENTER,
1094                Some("right") => RA_RIGHT,
1095                Some("justify") => RA_JUSTIFY,
1096                _ => RA_LEFT,
1097            };
1098            p.list = match s("list") {
1099                Some("bullet") => LS_BULLET,
1100                Some("number") => LS_NUMBER,
1101                _ => LS_NONE,
1102            };
1103            p.level = pv.get("level").and_then(|x| x.as_u64()).unwrap_or(0).min(LEVEL_MAX as u64) as u8;
1104            p.style = s("style").and_then(|n| STYLE_NAMES.iter().position(|&m| m == n)).unwrap_or(0) as u8;
1105            if let Some(runs) = pv.get("runs").and_then(|x| x.as_array()) {
1106                for r in runs {
1107                    let t = r.get("t").and_then(|x| x.as_str()).unwrap_or("");
1108                    p.push(t, &fmt_of_json(r));
1109                }
1110            }
1111            if let Some(m) = pv.get("mark") {
1112                p.mark = fmt_of_json(m);
1113            }
1114            paras.push(p);
1115        }
1116        let mut d = RichDoc::from_paras(paras);
1117        d.font = v.get("font").and_then(|x| x.as_str()).unwrap_or("").into();
1118        d.size = v.get("size").and_then(size_of_json).unwrap_or(0);
1119        Some(d)
1120    }
1121
1122    // ------------------------------------------------------------------ plain text
1123
1124    /// the text with list markers ("• ", "1. ") and two blanks of indent per level
1125    pub fn to_plain(&self) -> String {
1126        let marks = list_marks(&self.body.paras);
1127        let mut s = String::new();
1128        for (i, p) in self.body.paras.iter().enumerate() {
1129            if i > 0 {
1130                s.push('\n');
1131            }
1132            for _ in 0..p.level {
1133                s.push_str("  ");
1134            }
1135            if let Some(m) = &marks[i] {
1136                s.push_str(m);
1137                s.push(' ');
1138            }
1139            s.push_str(&p.text);
1140        }
1141        s
1142    }
1143}
1144
1145/// the list marker of every paragraph ("•", "◦", "▪", "1.", "a.", "i." by level; None: no
1146/// list item).  Numbers count the items of a level; a paragraph outside a list or an item
1147/// of another kind starts again.
1148pub fn list_marks(paras: &[Para]) -> Vec<Option<String>> {
1149    let mut count = [0u32; LEVEL_MAX as usize + 1];
1150    let mut kind = [LS_NONE; LEVEL_MAX as usize + 1];
1151    paras
1152        .iter()
1153        .map(|p| {
1154            if p.list == LS_NONE {
1155                count = [0; LEVEL_MAX as usize + 1];
1156                kind = [LS_NONE; LEVEL_MAX as usize + 1];
1157                return None;
1158            }
1159            let l = (p.level as usize).min(LEVEL_MAX as usize);
1160            for d in l + 1..=LEVEL_MAX as usize {
1161                count[d] = 0;
1162                kind[d] = LS_NONE;
1163            }
1164            if kind[l] != p.list {
1165                count[l] = 0;
1166                kind[l] = p.list;
1167            }
1168            count[l] += 1;
1169            Some(if p.list == LS_BULLET {
1170                String::from(["•", "◦", "▪"][l % 3])
1171            } else {
1172                match l % 3 {
1173                    0 => format!("{}.", count[l]),
1174                    1 => format!("{}.", alpha(count[l])),
1175                    _ => format!("{}.", roman(count[l])),
1176                }
1177            })
1178        })
1179        .collect()
1180}
1181
1182// a, b, .. z, aa, ab ..
1183fn alpha(n: u32) -> String {
1184    let mut n = n.max(1);
1185    let mut v = Vec::new();
1186    while n > 0 {
1187        n -= 1;
1188        v.push(b'a' + (n % 26) as u8);
1189        n /= 26;
1190    }
1191    v.reverse();
1192    String::from_utf8(v).unwrap_or_default()
1193}
1194
1195// i, ii, iii, iv ..
1196fn roman(n: u32) -> String {
1197    const T: [(u32, &str); 13] =
1198        [(1000, "m"), (900, "cm"), (500, "d"), (400, "cd"), (100, "c"), (90, "xc"), (50, "l"), (40, "xl"), (10, "x"), (9, "ix"), (5, "v"), (4, "iv"), (1, "i")];
1199    let mut n = n.clamp(1, 3999);
1200    let mut s = String::new();
1201    for (v, t) in T {
1202        while n >= v {
1203            s.push_str(t);
1204            n -= v;
1205        }
1206    }
1207    s
1208}
1209
1210const STYLE_NAMES: [&str; 5] = ["normal", "h1", "h2", "h3", "quote"];
1211
1212// the lines of a text (\r\n, \r and \n end a line)
1213pub(crate) fn lines_of(s: &str) -> impl Iterator<Item = &str> {
1214    s.split('\n').map(|l| l.strip_suffix('\r').unwrap_or(l)).flat_map(|l| l.split('\r'))
1215}
1216
1217fn big_limits() -> Limits {
1218    Limits { bytes: 64 << 20, depth: 8, values: 4 << 20, string_bytes: 16 << 20, members: 1 << 20 }
1219}
1220
1221/// #rrggbb (#rrggbbaa when not opaque)
1222pub(crate) fn color_hex(c: u32) -> String {
1223    let a = c >> 24;
1224    if a == 0xFF || a == 0 {
1225        format!("#{:06x}", c & 0xFF_FFFF)
1226    } else {
1227        format!("#{:06x}{:02x}", c & 0xFF_FFFF, a)
1228    }
1229}
1230
1231/// #rgb, #rrggbb, #rrggbbaa -> 0xAARRGGBB
1232pub(crate) fn parse_hex_color(s: &str) -> Option<u32> {
1233    let h = s.trim().strip_prefix('#')?;
1234    if !h.bytes().all(|b| b.is_ascii_hexdigit()) {
1235        return None;
1236    }
1237    let v = u32::from_str_radix(h, 16).ok()?;
1238    match h.len() {
1239        3 => Some(0xFF00_0000 | (v >> 8 & 15) * 0x11_0000 | (v >> 4 & 15) * 0x1100 | (v & 15) * 0x11),
1240        6 => Some(0xFF00_0000 | v),
1241        8 => Some((v & 0xFF) << 24 | v >> 8),
1242        _ => None,
1243    }
1244}
1245
1246// tenths of a point as JSON: 11, 14.5
1247fn size_json(t: u16) -> Value {
1248    match json::Number::parse(&size_text(t)) {
1249        Some(n) => Value::Number(n),
1250        None => Value::u64((t / 10) as u64),
1251    }
1252}
1253
1254/// tenths of a point as text: "11", "14.5"
1255pub(crate) fn size_text(t: u16) -> String {
1256    if t % 10 == 0 { format!("{}", t / 10) } else { format!("{}.{}", t / 10, t % 10) }
1257}
1258
1259/// "14.5" -> 145 (tenths)
1260pub(crate) fn parse_tenths(s: &str) -> Option<u16> {
1261    let s = s.trim();
1262    let (i, f) = s.split_once('.').unwrap_or((s, ""));
1263    let i: u32 = i.parse().ok()?;
1264    let f: u32 = f.chars().next().map_or(Some(0), |c| c.to_digit(10))?;
1265    Some((i.min(u16::MAX as u32) * 10 + f).min(u16::MAX as u32) as u16)
1266}
1267
1268fn size_of_json(v: &Value) -> Option<u16> {
1269    match v {
1270        Value::Number(n) => parse_tenths(n.as_str()),
1271        _ => None,
1272    }
1273}
1274
1275fn fmt_json(f: &Fmt) -> Value {
1276    let mut o = Value::Object(Vec::new());
1277    for (bit, k) in [(RF_BOLD, "b"), (RF_ITALIC, "i"), (RF_UNDER, "u"), (RF_STRIKE, "s")] {
1278        if f.flags & bit != 0 {
1279            o.insert(k, Value::u64(1));
1280        }
1281    }
1282    if f.color != 0 {
1283        o.insert("color", Value::string(color_hex(f.color)));
1284    }
1285    if f.bg != 0 {
1286        o.insert("bg", Value::string(color_hex(f.bg)));
1287    }
1288    if f.size != 0 {
1289        o.insert("size", size_json(f.size));
1290    }
1291    if !f.font.is_empty() {
1292        o.insert("font", Value::string(f.font.clone()));
1293    }
1294    if !f.link.is_empty() {
1295        o.insert("link", Value::string(f.link.clone()));
1296    }
1297    o
1298}
1299
1300fn fmt_of_json(v: &Value) -> Fmt {
1301    let mut f = Fmt::default();
1302    for (bit, k) in [(RF_BOLD, "b"), (RF_ITALIC, "i"), (RF_UNDER, "u"), (RF_STRIKE, "s")] {
1303        let on = match v.get(k) {
1304            Some(Value::Bool(b)) => *b,
1305            Some(x) => x.as_u64().is_some_and(|n| n != 0),
1306            None => false,
1307        };
1308        if on {
1309            f.flags |= bit;
1310        }
1311    }
1312    let s = |k: &str| v.get(k).and_then(|x| x.as_str());
1313    f.color = s("color").and_then(parse_hex_color).unwrap_or(0);
1314    f.bg = s("bg").and_then(parse_hex_color).unwrap_or(0);
1315    f.size = v.get("size").and_then(size_of_json).unwrap_or(0);
1316    f.font = s("font").unwrap_or("").into();
1317    f.link = s("link").unwrap_or("").into();
1318    f
1319}
1320
1321// (rich_edit.rs) the merge classes
1322pub(crate) const RK_TYPE: u8 = K_TYPE;
1323pub(crate) const RK_BACK: u8 = K_BACK;
1324pub(crate) const RK_DEL: u8 = K_DEL;