diff --git a/ivette/src/frama-c/richtext.tsx b/ivette/src/frama-c/richtext.tsx index aa96434523bfcc635fe253f699de218802b3ec58..77310554c80ef157931fa6497f60acf91c34a443 100644 --- a/ivette/src/frama-c/richtext.tsx +++ b/ivette/src/frama-c/richtext.tsx @@ -30,11 +30,121 @@ */ import React from 'react'; -import * as Dome from 'dome'; import * as KernelData from 'frama-c/kernel/api/data'; import { classes } from 'dome/misc/utils'; -const D = new Dome.Debug('Utils'); +// -------------------------------------------------------------------------- +// --- Kernel Text Utilities +// -------------------------------------------------------------------------- + +/** Unstructured text contents */ +export function textToString(text: KernelData.text): string { + if (text===null) return ''; + if (typeof(text)==='string') return text; + // documented to be faster than map & join + let buffer=''; + // skip tag mark + for(let k=1; k<text.length; k++) + buffer += textToString(text[k]); + return buffer; +} + +// -------------------------------------------------------------------------- +// --- Text Tag Tree +// -------------------------------------------------------------------------- + +/** Tag Tree */ +export type Tags = readonly Tag[]; +export type Tag = { + tag: string, + offset: number, + endOffset: number, + children: Tags +}; +export type TagIndex = Map<string, Tags>; + +/** Extract a Tag forest from a text. */ +export function textToTags( + text: KernelData.text, + filter?: (tag: string, offset: number, endOffset: number) => boolean, +): { + index: TagIndex, + tags: Tags +} { + const walk = + (buffer: Tag[], offset: number, text: KernelData.text): number => { + if (text===null) return offset; + if (typeof(text)==='string') return offset + text.length; + const t0 = text[0]; + const tag = typeof(t0)==='string' ? t0 : ''; + const children: Tag[] = []; + let endOffset = offset; + for(let k=1; k<text.length; k++) + endOffset = walk(children, endOffset, text[k]); + if (!filter || filter(tag, offset, endOffset)) { + const tg = { tag, offset, endOffset, children }; + const tgs = index.get(tag); + if (tgs===undefined) index.set(tag, [tg]); else tgs.push(tg); + buffer.push(tg); + } else { + children.forEach(tg => buffer.push(tg)); + } + return endOffset; + }; + const index = new Map<string, Tag[]>(); + const tags : Tag[] = []; + walk(tags, 0, text); + return { index, tags }; +} + +// -------------------------------------------------------------------------- +// --- Text Tag Index +// -------------------------------------------------------------------------- + +/** Lookup for the top-most tag containing the offset */ +export function findChildTag(tags: Tags, offset: number) : Tag | undefined +{ + let p = 0; + let q = tags.length - 1; + if (q < p) return undefined; + let a = tags[p]; + let b = tags[q]; + if (offset < a.offset) return undefined; + if (offset > b.endOffset) return undefined; + if (offset <= a.endOffset) return a; + if (b.offset <= offset) return b; + // @invariant Range: 0 <= p <= q < tags.length; + // @invariant Tags: a = tags[p] /\ b = tags[q]; + // @invariant Offset: a.endOffset < offset < b.offset; + // @variant q-p; + for(;;) { + const d = q-p; + if (d <= 1) return undefined; + const r = p + d / 2; + const c = tags[r]; + if (offset < c.offset) { b = c; q = r; continue; } + if (c.endOffset < offset) { a = c; p = r; continue; } + return c; + } +} + +/** Lookup for the deepest tag containing the offset and satisfying the + filtering condition. */ +export function findTag( + tags: Tags, + offset: number, + filter?: (tag: Tag) => boolean, +): Tag | undefined { + type Result = Tag | undefined; + const lookup = (tags: Tags, res: Result): Result => { + const r = findChildTag(tags, offset); + if (r && (!filter || filter(r))) + return lookup(r.children, r); + else + return res; + }; + return lookup(tags, undefined); +} // -------------------------------------------------------------------------- // --- Lightweight Text Renderer @@ -80,13 +190,13 @@ export interface TextProps { export function Text(props: TextProps): JSX.Element { const className = classes('kernel-text', 'dome-text-code', props.className); - function makeContents(text: KernelData.text): React.ReactNode { + const makeContents = (text: KernelData.text): React.ReactNode => { if (Array.isArray(text)) { const tag = text[0]; const marker = tag && typeof (tag) === 'string'; const array = marker ? text.slice(1) : text; const contents = React.Children.toArray(array.map(makeContents)); - if (marker) { + if (marker) return ( <Marker marker={tag} @@ -96,14 +206,10 @@ export function Text(props: TextProps): JSX.Element { {contents} </Marker> ); - } return <>{contents}</>; - } if (typeof text === 'string') { - return text; } - D.error('Unexpected text', text); - return null; - } + return text; + }; return <div className={className}>{makeContents(props.text)}</div>; }