Compare commits

...

11 Commits

Author SHA1 Message Date
Tom Alexander
6db3dc65fa Merge branch 'dark_mode' 2024-01-28 19:11:18 -05:00
Tom Alexander
43d65b4fe3 Disable spellcheck to remove red squiggles. 2024-01-28 19:11:07 -05:00
Tom Alexander
ff3618b67d Add dark mode support for the editor. 2024-01-28 19:09:41 -05:00
Tom Alexander
8c2c48a719 Add dark mode support for the AST tree. 2024-01-28 19:04:26 -05:00
Tom Alexander
43de54039b Move the shadow to the left.
This is to make indentation depth easier to see.
2024-01-28 18:31:33 -05:00
Tom Alexander
acaf757ce3 Merge branch 'isolate_editor' 2024-01-28 18:24:24 -05:00
Tom Alexander
1cc5dd7911 Move flexbox handling up to the explorer component. 2024-01-28 18:21:38 -05:00
Tom Alexander
083b0aa376 Move explorer styles into local styles. 2024-01-28 18:20:22 -05:00
Tom Alexander
d158fafd77 Move the highlightable textarea into its own component. 2024-01-28 18:14:57 -05:00
Tom Alexander
33ed9c4f56 Add types to Explorer props. 2024-01-28 17:32:27 -05:00
Tom Alexander
a11201363e Rename Editor to Explorer.
This component was both the editor and the AST view so it doesn't make sense to call it Editor. Instead the highlightable textarea is going to be moved into a new Editor component.
2024-01-28 17:28:43 -05:00
9 changed files with 205 additions and 111 deletions

View File

@@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import Editor from "./Editor"; import Explorer from "./Explorer";
import styles from "./App.module.css"; import styles from "./App.module.css";
import "./reset.css"; import "./reset.css";
function App({}) { function App({}) {
return ( return (
<div className={styles.App}> <div className={styles.App}>
<Editor /> <Explorer />
</div> </div>
); );
} }

View File

@@ -1,15 +1,19 @@
.Editor { .EditorTextWrapper {
display: flex; --editor-background-color: #000000;
flex-direction: row; --editor-font-color: #ffffff;
height: 100%; --editor-highlight-color: #0000ff;
width: 100%; }
@media (prefers-color-scheme: light) {
.EditorTextWrapper {
--editor-background-color: #ffffff;
--editor-font-color: #000000;
--editor-highlight-color: #ffff00;
}
} }
.Editor-textwrapper { .EditorTextWrapper {
flex: 1;
flex-basis: 0;
position: relative; position: relative;
background: white; background: var(--editor-background-color);
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
"DejaVu Sans Mono", monospace; "DejaVu Sans Mono", monospace;
font-weight: normal; font-weight: normal;
@@ -17,8 +21,9 @@
text-align: initial; text-align: initial;
} }
.Editor-textarea, .EditorTextArea,
.Editor-underlay { .EditorUnderlay {
color: var(--editor-font-color);
font-family: inherit; font-family: inherit;
font-weight: inherit; font-weight: inherit;
font-size: inherit; font-size: inherit;
@@ -28,7 +33,7 @@
line-height: normal; line-height: normal;
} }
.Editor-textarea { .EditorTextArea {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
@@ -43,7 +48,7 @@
outline: none; outline: none;
} }
.Editor-underlay { .EditorUnderlay {
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
@@ -52,7 +57,7 @@
word-wrap: break-word; word-wrap: break-word;
overflow-y: scroll; overflow-y: scroll;
.highlighted { .EditorHighlighted {
background: #ffff00; background: var(--editor-highlight-color);
} }
} }

4
src/Editor.module.css.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export const EditorTextWrapper: string;
export const EditorTextArea: string;
export const EditorUnderlay: string;
export const EditorHighlighted: string;

View File

@@ -1,68 +1,25 @@
import React, { ReactNode, useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useRef } from "react";
import "./Editor.css";
import { Highlight } from "./highlight"; import { Highlight } from "./highlight";
import { buildShadow } from "./shadow"; import { buildShadow } from "./shadow";
import OrgAst, { OrgNodeReference } from "./OrgAst"; import styles from "./Editor.module.css";
import { parse_org } from "../../organic/target/wasm32-unknown-unknown/js/wasm";
const default_org_source: string = `* Welcome to the Organic Ast Explorer! interface EditorProps {
value: string;
Type your Org [fn:1] source in this text box, and it will be parsed by Organic [fn:2] that has been compiled into wasm and embedded in this page. The resulting AST will be rendered to the right. setValue: Function;
highlights: Highlight[];
In the AST on the right, you can: clearHighlights: Function;
}
1. Click on an AST node to highlight the corresponding portion of the Org source on the left. function Editor({
2. Expand/Collapse the children, properties, and standard properties. value,
setValue,
* Footnotes highlights,
clearHighlights,
[fn:1] https://orgmode.org/ }: EditorProps): React.ReactNode {
[fn:2] https://code.fizz.buzz/talexander/organic
`;
function Editor({ defaultValue = default_org_source }) {
function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) { function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
setValue(event.target.value); setValue(event.target.value);
clearHighlights(); clearHighlights();
} }
const [value, setValue] = useState(defaultValue);
const [highlights, setHighlights] = useState<Array<Highlight>>([]);
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);
}
function addHighlight(start: number, end: number) {
let new_highlights = [...highlights, new Highlight(start, end)];
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);
}
function clearHighlights() {
setHighlights([]);
}
const textAreaRef = useRef<HTMLTextAreaElement>(null); const textAreaRef = useRef<HTMLTextAreaElement>(null);
const shadowRef = useRef<HTMLDivElement>(null); const shadowRef = useRef<HTMLDivElement>(null);
function onTextAreaScroll() { function onTextAreaScroll() {
@@ -78,26 +35,19 @@ function Editor({ defaultValue = default_org_source }) {
}, []); }, []);
return ( return (
<div className="Editor"> <div className={styles.EditorTextWrapper}>
<div className="Editor-textwrapper"> <textarea
<textarea ref={textAreaRef}
ref={textAreaRef} onChange={handleChange}
onChange={handleChange} className={styles.EditorTextArea}
className="Editor-textarea"
value={value}
onScroll={onTextAreaScroll}
/>
<div ref={shadowRef} className="Editor-underlay">
{buildShadow(highlights, value)}
<br />
</div>
</div>
<OrgAst
setHighlight={setHighlight}
clearHighlights={clearHighlights}
value={value} value={value}
astTree={astTree} onScroll={onTextAreaScroll}
spellCheck={false}
/> />
<div ref={shadowRef} className={styles.EditorUnderlay}>
{buildShadow(highlights, value)}
<br />
</div>
</div> </div>
); );
} }

11
src/Explorer.module.css Normal file
View File

@@ -0,0 +1,11 @@
.Explorer {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
> * {
flex: 1;
flex-basis: 0;
}
}

1
src/Explorer.module.css.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export const Explorer: string;

82
src/Explorer.tsx Normal file
View File

@@ -0,0 +1,82 @@
import React, { useMemo, useState } from "react";
import styles from "./Explorer.module.css";
import { Highlight } from "./highlight";
import OrgAst, { OrgNodeReference } from "./OrgAst";
import { parse_org } from "../../organic/target/wasm32-unknown-unknown/js/wasm";
import Editor from "./Editor";
const default_org_source: string = `* Welcome to the Organic Ast Explorer!
Type your Org [fn:1] source in this text box, and it will be parsed by Organic [fn:2] that has been compiled into wasm and embedded in this page. The resulting AST will be rendered to the right.
In the AST on the right, you can:
1. Click on an AST node to highlight the corresponding portion of the Org source on the left.
2. Expand/Collapse the children, properties, and standard properties.
* Footnotes
[fn:1] https://orgmode.org/
[fn:2] https://code.fizz.buzz/talexander/organic
`;
interface ExplorerProps {
defaultValue?: string;
}
function Explorer({ defaultValue = default_org_source }: ExplorerProps) {
const [value, setValue] = useState(defaultValue);
const [highlights, setHighlights] = useState<Array<Highlight>>([]);
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);
}
function addHighlight(start: number, end: number) {
let new_highlights = [...highlights, new Highlight(start, end)];
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);
}
function clearHighlights() {
setHighlights([]);
}
return (
<div className={styles.Explorer}>
<Editor
value={value}
setValue={setValue}
highlights={highlights}
clearHighlights={clearHighlights}
/>
<OrgAst
setHighlight={setHighlight}
clearHighlights={clearHighlights}
value={value}
astTree={astTree}
/>
</div>
);
}
export default Explorer;

View File

@@ -1,17 +1,57 @@
.OrgAst { .OrgAst {
flex: 1; --ast-background-color: #111111;
background: #eeeeee; --ast-font-color: #ffffff;
--ast-node-border-color: #bbbbbb;
--ast-node-shadow-color: #bbbbbb;
--ast-node-type-background-color: #933009;
--ast-node-type-hover-background-color: #8f0745;
--ast-node-type-selected-background-color: #630368;
--ast-node-subsection-border-color: #ffffff;
--ast-node-children-background-color: #111111;
--ast-node-table-row-odd-background-color: #111111;
--ast-node-table-row-even-background-color: #000000;
}
@media (prefers-color-scheme: light) {
.OrgAst {
--ast-background-color: #eeeeee;
--ast-font-color: #000000;
--ast-node-background-color: #ffffff;
--ast-node-border-color: #000000;
--ast-node-shadow-color: #000000;
--ast-node-type-background-color: #6ccff6;
--ast-node-type-hover-background-color: #70f8ba;
--ast-node-type-selected-background-color: #9cfc97;
--ast-node-subsection-border-color: #000000;
--ast-node-children-background-color: #eeeeee;
--ast-node-table-row-odd-background-color: #eeeeee;
--ast-node-table-row-even-background-color: #ffffff;
}
}
.OrgAst {
background: var(--ast-background-color);
color: var(--ast-font-color);
padding: 5px; padding: 5px;
overflow: auto; overflow: auto;
} }
.OrgAstNode { .OrgAstNode {
border: 1px solid #000000; border: 1px solid var(--ast-node-border-color);
background: #ffffff; background: var(--ast-node-background-color);
box-shadow: 3px 3px 4px #000000; box-shadow: -3px 3px 1px var(--ast-node-shadow-color);
> details { > details {
border: 1px solid #000000; border: 1px solid var(--ast-node-subsection-border-color);
margin: 5px 5px 5px 2px; margin: 5px 5px 5px 2px;
> summary { > summary {
@@ -24,25 +64,25 @@
> summary { > summary {
border-width: 0 0 1px 0; border-width: 0 0 1px 0;
border-style: dotted; border-style: dotted;
border-color: #000000; border-color: var(--ast-node-subsection-border-color);
} }
} }
} }
.OrgAstNode.selected { .OrgAstNode.selected {
> .OrgAstNodeType { > .OrgAstNodeType {
background: #9cfc97; background: var(--ast-node-type-selected-background-color);
} }
} }
.OrgAstNode.hovered:not(.selected) { .OrgAstNode.hovered:not(.selected) {
> .OrgAstNodeType { > .OrgAstNodeType {
background: #70f8ba; background: var(--ast-node-type-hover-background-color);
} }
} }
.OrgAstNodeType { .OrgAstNodeType {
background: #6ccff6; background: var(--ast-node-type-background-color);
padding: 3px; padding: 3px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
@@ -65,20 +105,20 @@
.OrgAstChildren { .OrgAstChildren {
padding: 5px 5px 5px 20px; padding: 5px 5px 5px 20px;
background: #eeeeee; background: var(--ast-node-children-background-color);
} }
.OrgAstProperties, .OrgAstProperties,
.OrgAstObjectTree, .OrgAstObjectTree,
.OrgAstOptionalPair { .OrgAstOptionalPair {
border: 1px solid #000000; border: 1px solid var(--ast-node-subsection-border-color);
margin: 5px; margin: 5px;
> tbody { > tbody {
> tr { > tr {
border-width: 1px 0; border-width: 1px 0;
border-style: solid; border-style: solid;
border-color: #000000; border-color: var(--ast-node-subsection-border-color);
> th, > th,
> td { > td {
@@ -93,10 +133,10 @@
} }
> tr:nth-child(odd) { > tr:nth-child(odd) {
background-color: #eeeeee; background-color: var(--ast-node-table-row-odd-background-color);
} }
> tr:nth-child(even) { > tr:nth-child(even) {
background-color: #ffffff; background-color: var(--ast-node-table-row-even-background-color);
} }
} }
} }
@@ -104,7 +144,7 @@
.OrgAstObjectTree { .OrgAstObjectTree {
> tbody { > tbody {
border-style: dashed; border-style: dashed;
border-color: #000000; border-color: var(--ast-node-subsection-border-color);
} }
> tbody:not(:first-child, :last-child) { > tbody:not(:first-child, :last-child) {
border-width: 3px 0; border-width: 3px 0;

View File

@@ -1,5 +1,6 @@
import React, { ReactNode, useState } from "react"; import React, { ReactNode } from "react";
import { Highlight } from "./highlight"; import { Highlight } from "./highlight";
import styles from "./Editor.module.css";
function buildShadow(highlights: Highlight[], text: string): ReactNode[] { function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
let output: ReactNode[] = []; let output: ReactNode[] = [];
@@ -22,7 +23,7 @@ function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
} else if (state == ShadowState.Highlight && !thisCharHighlighted) { } else if (state == ShadowState.Highlight && !thisCharHighlighted) {
// End the span // End the span
output.push( output.push(
<span key={i} className="highlighted"> <span key={i} className={styles.EditorHighlighted}>
{buffer} {buffer}
</span>, </span>,
); );
@@ -37,7 +38,7 @@ function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
output.push(buffer); output.push(buffer);
} else if (state == ShadowState.Highlight) { } else if (state == ShadowState.Highlight) {
output.push( output.push(
<span key={i} className="highlighted"> <span key={i} className={styles.EditorHighlighted}>
{buffer} {buffer}
</span>, </span>,
); );