Compare commits

...

10 Commits

Author SHA1 Message Date
Tom Alexander
f60725b341
Use monospace font for source. 2024-01-27 22:15:31 -05:00
Tom Alexander
d73f344dd0
Compact form for plain text nodes. 2024-01-27 22:04:22 -05:00
Tom Alexander
04b79631d7
Get rid of flexbox. 2024-01-27 21:51:06 -05:00
Tom Alexander
8f360fda4b
Show the portion of the original source for the AST node in the AST tree. 2024-01-27 21:46:44 -05:00
Tom Alexander
bb15dbcbaf
Add support for optional pairs. 2024-01-27 20:49:43 -05:00
Tom Alexander
49905f1273
Lock the scroll of the div and textarea to keep the highlighting accurate. 2024-01-27 20:16:59 -05:00
Tom Alexander
cc48040f11
Only re-parse the ast when value changes. 2024-01-27 19:31:29 -05:00
Tom Alexander
8627181480
Fix creation of shadow div.
If two highlights started at the same index, it would sometimes not render both of them.
2024-01-27 19:22:31 -05:00
Tom Alexander
5feac8e4d7
Fix a missing key. 2024-01-27 18:57:01 -05:00
Tom Alexander
0e02e09018
Add support for highlighting when hovering over a node. 2024-01-27 18:52:18 -05:00
6 changed files with 267 additions and 66 deletions

View File

@ -49,6 +49,8 @@
pointer-events: none;
color: transparent;
white-space: pre-wrap;
word-wrap: break-word;
overflow-y: scroll;
.highlighted {
background: #ffff00;

View File

@ -1,8 +1,8 @@
import React, { ReactNode, useState } from "react";
import React, { ReactNode, useMemo, useRef, useState } from "react";
import "./Editor.css";
import { Highlight } from "./highlight";
import { buildShadow } from "./shadow";
import OrgAst from "./OrgAst";
import OrgAst, { OrgNodeReference } from "./OrgAst";
import { parse_org } from "../../organic/target/wasm32-unknown-unknown/js/wasm";
const default_org_source: string = `Welcome to the Organic Wasm Demo!
@ -31,8 +31,21 @@ function Editor({ defaultValue = default_org_source }) {
const [highlights, setHighlights] = useState<Array<Highlight>>([]);
function setHighlight(start: number, end: number) {
let new_highlights = [new Highlight(start, end)];
const astTree = useMemo(() => {
const astTree = parse_org(value);
console.log(JSON.stringify(astTree));
return astTree;
}, [value]);
function setHighlight(nodes: OrgNodeReference[]) {
let new_highlights = nodes.map((node: OrgNodeReference) => {
return new Highlight(node.start - 1, node.end - 1);
});
new_highlights.sort(function (a, b) {
if (a.start < b.start) return -1;
if (a.start > b.start) return 1;
return 0;
});
setHighlights(new_highlights);
}
@ -50,18 +63,29 @@ function Editor({ defaultValue = default_org_source }) {
setHighlights([]);
}
const astTree = parse_org(value);
console.log(JSON.stringify(astTree));
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const shadowRef = useRef<HTMLDivElement>(null);
function onTextAreaScroll() {
if (shadowRef.current !== null && textAreaRef.current !== null) {
const textAreaScrollTop = textAreaRef.current.scrollTop;
shadowRef.current.scrollTop = textAreaScrollTop;
}
}
return (
<div className="Editor">
<div className="Editor-textwrapper">
<textarea
ref={textAreaRef}
onChange={handleChange}
className="Editor-textarea"
value={value}
onScroll={onTextAreaScroll}
/>
<div className="Editor-underlay">{buildShadow(highlights, value)}</div>
<div ref={shadowRef} className="Editor-underlay">
{buildShadow(highlights, value)}
<br />
</div>
</div>
<OrgAst
setHighlight={setHighlight}

View File

@ -2,7 +2,7 @@
flex: 1;
background: #eeeeee;
padding: 5px;
overflow: scroll;
overflow: auto;
}
.OrgAstNode {
@ -35,11 +35,31 @@
}
}
.OrgAstNode.hovered:not(.selected) {
> .OrgAstNodeType {
background: #70f8ba;
}
}
.OrgAstNodeType {
font-size: 1.3rem;
font-weight: 700;
background: #6ccff6;
padding: 3px;
overflow: hidden;
> span:first-child {
font-size: 1.1rem;
font-weight: 700;
white-space: nowrap;
}
> span:nth-child(2) {
margin-left: 1rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo,
Consolas, "DejaVu Sans Mono", monospace;
font-size: 0.8rem;
}
}
.OrgAstChildren {
@ -48,7 +68,8 @@
}
.OrgAstProperties,
.OrgAstObjectTree {
.OrgAstObjectTree,
.OrgAstOptionalPair {
border: 1px solid #000000;
margin: 5px;

View File

@ -3,5 +3,7 @@ export const OrgAstNode: string;
export const OrgAstNodeType: string;
export const OrgAstChildren: string;
export const selected: string;
export const hovered: string;
export const OrgAstProperties: string;
export const OrgAstObjectTree: string;
export const OrgAstOptionalPair: string;

View File

@ -8,11 +8,28 @@ const OrgAst = (props: {
astTree: any;
value: string;
}) => {
const [selectedNode, setSelectedNode] = useState<string>("");
const [selectedNode, setSelectedNode] = useState<OrgNodeReference | null>(
null,
);
const [hoveredNode, setHoveredNode] = useState<OrgNodeReference | null>(null);
function selectNode(uid: string, start: number, end: number) {
props.setHighlight(start, end);
setSelectedNode(uid);
const new_node: OrgNodeReference = { uid: uid, start: start, end: end };
props.setHighlight([new_node, hoveredNode].filter((node) => node !== null));
setSelectedNode({ uid: uid, start: start, end: end });
}
function startHoverNode(uid: string, start: number, end: number) {
const new_node: OrgNodeReference = { uid: uid, start: start, end: end };
props.setHighlight(
[selectedNode, new_node].filter((node) => node !== null),
);
setHoveredNode({ uid: uid, start: start, end: end });
}
function endHoverNode(uid: string) {
props.setHighlight([selectedNode].filter((node) => node !== null));
setHoveredNode(null);
}
if (props.astTree.status !== "success") {
@ -24,45 +41,127 @@ const OrgAst = (props: {
key="^"
uid="^"
selectNode={selectNode}
startHoverNode={startHoverNode}
endHoverNode={endHoverNode}
node={props.astTree.content}
selectedNode={selectedNode}
fullSource={props.value}
/>
</div>
);
}
};
interface OrgNodeReference {
uid: string;
start: number;
end: number;
}
const OrgAstNode = (props: {
selectNode: Function;
startHoverNode: Function;
endHoverNode: Function;
node: any;
uid: string;
selectedNode: string;
selectedNode: OrgNodeReference | null;
fullSource: string;
}) => {
const [isHovered, setIsHovered] = useState(false);
function selectNode() {
props.selectNode(
props.uid,
props.node["standard-properties"]["begin"] - 1,
props.node["standard-properties"]["end"] - 1,
props.node["standard-properties"]["begin"],
props.node["standard-properties"]["end"],
);
}
const nodeClassName =
props.selectedNode === props.uid
? styles.OrgAstNode + " " + styles.selected
: styles.OrgAstNode;
function hoverNode() {
props.startHoverNode(
props.uid,
props.node["standard-properties"]["begin"],
props.node["standard-properties"]["end"],
);
setIsHovered(true);
}
function endHoverNode() {
props.endHoverNode(props.uid);
setIsHovered(false);
}
function unicodeAwareSlice(text: string, start: number, end: number) {
// Boooo javascript
let i = 0;
let output = "";
for (const chr of text) {
if (i >= end) {
break;
}
if (i >= start) {
output += chr;
}
++i;
}
return output;
}
let nodeClassName = styles.OrgAstNode;
if (props.selectedNode?.uid === props.uid) {
nodeClassName = nodeClassName + " " + styles.selected;
}
if (isHovered) {
nodeClassName = nodeClassName + " " + styles.hovered;
}
const selfSource = JSON.stringify(
unicodeAwareSlice(
props.fullSource,
props.node["standard-properties"].begin - 1,
props.node["standard-properties"].end - 1,
),
);
if (props.node["ast-node"] === "plain-text") {
return (
<div className={nodeClassName}>
<div
className={styles.OrgAstNodeType}
onClick={selectNode}
onMouseEnter={hoverNode}
onMouseLeave={endHoverNode}
title={selfSource}
>
<span>{props.node["ast-node"]}</span>
<span>{selfSource}</span>
</div>
</div>
);
}
return (
<div className={nodeClassName}>
<div className={styles.OrgAstNodeType} onClick={selectNode}>
{props.node["ast-node"]}
<div
className={styles.OrgAstNodeType}
onClick={selectNode}
onMouseEnter={hoverNode}
onMouseLeave={endHoverNode}
title={selfSource}
>
<span>{props.node["ast-node"]}</span>
<span>{selfSource}</span>
</div>
<details>
<summary>Standard Properties</summary>
<OrgPropertiesList
selectNode={props.selectNode}
startHoverNode={props.startHoverNode}
endHoverNode={props.endHoverNode}
parentUniqueId={props.uid}
selectedNode={props.selectedNode}
properties={props.node["standard-properties"]}
fullSource={props.fullSource}
/>
</details>
{!!Object.keys(props.node.properties).length ? (
@ -71,9 +170,12 @@ const OrgAstNode = (props: {
<summary>Properties</summary>
<OrgPropertiesList
selectNode={props.selectNode}
startHoverNode={props.startHoverNode}
endHoverNode={props.endHoverNode}
parentUniqueId={props.uid}
selectedNode={props.selectedNode}
properties={props.node.properties}
fullSource={props.fullSource}
/>
</details>
</>
@ -84,9 +186,12 @@ const OrgAstNode = (props: {
<div className={styles.OrgAstChildren}>
<OrgAstNodeList
selectNode={props.selectNode}
startHoverNode={props.startHoverNode}
endHoverNode={props.endHoverNode}
parentUniqueId={props.uid}
selectedNode={props.selectedNode}
node_list={props.node.children}
fullSource={props.fullSource}
/>
</div>
</details>
@ -97,9 +202,12 @@ const OrgAstNode = (props: {
const OrgAstNodeList = (props: {
selectNode: Function;
startHoverNode: Function;
endHoverNode: Function;
parentUniqueId: string;
selectedNode: string;
selectedNode: OrgNodeReference | null;
node_list: any[];
fullSource: string;
}): React.JSX.Element[] => {
return props.node_list.map((node) => {
const uid =
@ -116,8 +224,11 @@ const OrgAstNodeList = (props: {
key={uid}
uid={uid}
selectNode={props.selectNode}
startHoverNode={props.startHoverNode}
endHoverNode={props.endHoverNode}
selectedNode={props.selectedNode}
node={node}
fullSource={props.fullSource}
/>
);
});
@ -125,9 +236,12 @@ const OrgAstNodeList = (props: {
const OrgPropertiesList = (props: {
selectNode: Function;
startHoverNode: Function;
endHoverNode: Function;
parentUniqueId: string;
selectedNode: string;
selectedNode: OrgNodeReference | null;
properties: Object;
fullSource: string;
}): React.JSX.Element => {
const entries = Object.entries(props.properties)
.sort((a, b) => {
@ -150,9 +264,12 @@ const OrgPropertiesList = (props: {
<td>
<OrgPropertyValue
selectNode={props.selectNode}
startHoverNode={props.startHoverNode}
endHoverNode={props.endHoverNode}
parentUniqueId={props.parentUniqueId}
selectedNode={props.selectedNode}
value={value}
fullSource={props.fullSource}
/>
</td>
</tr>
@ -168,9 +285,12 @@ const OrgPropertiesList = (props: {
const OrgPropertyValue = (props: {
selectNode: Function;
startHoverNode: Function;
endHoverNode: Function;
parentUniqueId: string;
selectedNode: string;
selectedNode: OrgNodeReference | null;
value: any;
fullSource: string;
}): React.ReactNode => {
if (
props.value === null ||
@ -183,19 +303,40 @@ const OrgPropertyValue = (props: {
<div className={styles.OrgAstChildren}>
<OrgAstNodeList
selectNode={props.selectNode}
startHoverNode={props.startHoverNode}
endHoverNode={props.endHoverNode}
parentUniqueId={props.parentUniqueId}
selectedNode={props.selectedNode}
node_list={props.value}
fullSource={props.fullSource}
/>
</div>
);
} else if (is_optional_pair(props.value)) {
return (
<table className={styles.OrgAstOptionalPair}>
<tbody>
<tr>
<th scope="row">Optional value:</th>
<td>{JSON.stringify(props.value.optval)}</td>
</tr>
<tr>
<th scope="row">Value:</th>
<td>{JSON.stringify(props.value.val)}</td>
</tr>
</tbody>
</table>
);
} else if (is_object_tree(props.value)) {
return (
<OrgObjectTree
selectNode={props.selectNode}
startHoverNode={props.startHoverNode}
endHoverNode={props.endHoverNode}
parentUniqueId={props.parentUniqueId}
selectedNode={props.selectedNode}
value={props.value}
fullSource={props.fullSource}
/>
);
} else {
@ -205,16 +346,22 @@ const OrgPropertyValue = (props: {
interface OrgObjectTreeProps {
selectNode: Function;
startHoverNode: Function;
endHoverNode: Function;
parentUniqueId: string;
selectedNode: string;
selectedNode: OrgNodeReference | null;
value: any;
fullSource: string;
}
function OrgObjectTree({
selectNode,
startHoverNode,
endHoverNode,
parentUniqueId,
selectedNode,
value,
fullSource,
}: OrgObjectTreeProps): React.ReactNode {
const entries = value["object-tree"].map((entry: any) => {
return (
@ -224,9 +371,12 @@ function OrgObjectTree({
<td>
<OrgAstNodeList
selectNode={selectNode}
startHoverNode={startHoverNode}
endHoverNode={endHoverNode}
parentUniqueId={parentUniqueId}
selectedNode={selectedNode}
node_list={entry[0]}
fullSource={fullSource}
/>
</td>
</tr>
@ -235,9 +385,12 @@ function OrgObjectTree({
<td>
<OrgAstNodeList
selectNode={selectNode}
startHoverNode={startHoverNode}
endHoverNode={endHoverNode}
parentUniqueId={parentUniqueId}
selectedNode={selectedNode}
node_list={entry[1]}
fullSource={fullSource}
/>
</td>
</tr>
@ -271,8 +424,14 @@ function is_list_of_ast_nodes(val: any): boolean {
});
}
function is_optional_pair(val: any): boolean {
return (
is_object(val) && val.hasOwnProperty("optval") && val.hasOwnProperty("val")
);
}
function is_object_tree(val: any): boolean {
return is_object(val) && val.hasOwnProperty("object-tree");
}
export default OrgAst;
export { OrgAst as default, OrgNodeReference };

View File

@ -2,36 +2,32 @@ import React, { ReactNode, useState } from "react";
import { Highlight } from "./highlight";
function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
let remaining_highlights = highlights.slice();
let output: ReactNode[] = [];
let i = 0;
let state = ShadowState.Text;
let buffer = "";
for (let chr of text) {
if (state == ShadowState.Text) {
if (
remaining_highlights.length > 0 &&
i == remaining_highlights[0].start
) {
// Start a span
output.push(buffer);
buffer = chr;
state = ShadowState.Highlight;
} else {
// Add a character
buffer += chr;
}
} else if (state == ShadowState.Highlight) {
if (remaining_highlights.length > 0 && i == remaining_highlights[0].end) {
// End the span
output.push(<span className="highlighted">{buffer}</span>);
buffer = chr;
state = ShadowState.Text;
remaining_highlights = remaining_highlights.slice(1);
} else {
// Add a character
buffer += chr;
}
const thisCharHighlighted = isInHighlight(highlights, i);
if (state == ShadowState.Text && thisCharHighlighted) {
// Start a span
output.push(buffer);
buffer = chr;
state = ShadowState.Highlight;
} else if (state == ShadowState.Text && !thisCharHighlighted) {
// Add a character
buffer += chr;
} else if (state == ShadowState.Highlight && thisCharHighlighted) {
// Add a character
buffer += chr;
} else if (state == ShadowState.Highlight && !thisCharHighlighted) {
// End the span
output.push(
<span key={i} className="highlighted">
{buffer}
</span>,
);
buffer = chr;
state = ShadowState.Text;
}
++i;
}
@ -40,26 +36,23 @@ function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
if (state == ShadowState.Text) {
output.push(buffer);
} else if (state == ShadowState.Highlight) {
output.push(<span className="highlighted">{buffer}</span>);
output.push(
<span key={i} className="highlighted">
{buffer}
</span>,
);
}
}
return output;
}
function unicodeAwareSlice(text: string, start: number, end: number): string {
// Boooo javascript
let i = 0;
let output = "";
for (let chr of text) {
if (i >= end) {
break;
function isInHighlight(highlights: Highlight[], offset: number): boolean {
for (const highlight of highlights) {
if (highlight.start <= offset && offset < highlight.end) {
return true;
}
if (i >= start) {
output += chr;
}
++i;
}
return output;
return false;
}
const enum ShadowState {