Compare commits
17 Commits
e881102ade
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6db3dc65fa | ||
|
|
43d65b4fe3 | ||
|
|
ff3618b67d | ||
|
|
8c2c48a719 | ||
|
|
43de54039b | ||
|
|
acaf757ce3 | ||
|
|
1cc5dd7911 | ||
|
|
083b0aa376 | ||
|
|
d158fafd77 | ||
|
|
33ed9c4f56 | ||
|
|
a11201363e | ||
|
|
3b3ef70d3b | ||
|
|
6129bdad3e | ||
|
|
f89e62b9e1 | ||
|
|
5d31d50863 | ||
|
|
c2850fa879 | ||
|
|
d5020d3f24 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
|
||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -28,7 +28,8 @@
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-merge": "^5.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"description": "",
|
||||
"main": "index.jsx",
|
||||
"scripts": {
|
||||
"dev": "webpack server",
|
||||
"format": "prettier --write '**/*.{json,js,ts,tsx,css}'"
|
||||
"dev": "webpack server --config webpack.dev.js",
|
||||
"format": "prettier --write '**/*.{json,js,ts,tsx,css}'",
|
||||
"build": "webpack --config webpack.dev.js",
|
||||
"release": "webpack --config webpack.prd.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -30,6 +32,7 @@
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-merge": "^5.10.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import Editor from "./Editor";
|
||||
import Explorer from "./Explorer";
|
||||
import styles from "./App.module.css";
|
||||
import "./reset.css";
|
||||
|
||||
function App({}) {
|
||||
return (
|
||||
<div className={styles.App}>
|
||||
<Editor />
|
||||
<Explorer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
.Editor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.EditorTextWrapper {
|
||||
--editor-background-color: #000000;
|
||||
--editor-font-color: #ffffff;
|
||||
--editor-highlight-color: #0000ff;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
.EditorTextWrapper {
|
||||
--editor-background-color: #ffffff;
|
||||
--editor-font-color: #000000;
|
||||
--editor-highlight-color: #ffff00;
|
||||
}
|
||||
}
|
||||
|
||||
.Editor-textwrapper {
|
||||
flex: 1;
|
||||
flex-basis: 0;
|
||||
.EditorTextWrapper {
|
||||
position: relative;
|
||||
background: white;
|
||||
background: var(--editor-background-color);
|
||||
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
|
||||
"DejaVu Sans Mono", monospace;
|
||||
font-weight: normal;
|
||||
@@ -17,8 +21,9 @@
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.Editor-textarea,
|
||||
.Editor-underlay {
|
||||
.EditorTextArea,
|
||||
.EditorUnderlay {
|
||||
color: var(--editor-font-color);
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
@@ -28,7 +33,7 @@
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.Editor-textarea {
|
||||
.EditorTextArea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
@@ -43,7 +48,7 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.Editor-underlay {
|
||||
.EditorUnderlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
@@ -52,7 +57,7 @@
|
||||
word-wrap: break-word;
|
||||
overflow-y: scroll;
|
||||
|
||||
.highlighted {
|
||||
background: #ffff00;
|
||||
.EditorHighlighted {
|
||||
background: var(--editor-highlight-color);
|
||||
}
|
||||
}
|
||||
4
src/Editor.module.css.d.ts
vendored
Normal file
4
src/Editor.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export const EditorTextWrapper: string;
|
||||
export const EditorTextArea: string;
|
||||
export const EditorUnderlay: string;
|
||||
export const EditorHighlighted: string;
|
||||
@@ -1,68 +1,25 @@
|
||||
import React, { ReactNode, useMemo, useRef, useState } from "react";
|
||||
import "./Editor.css";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Highlight } from "./highlight";
|
||||
import { buildShadow } from "./shadow";
|
||||
import OrgAst, { OrgNodeReference } from "./OrgAst";
|
||||
import { parse_org } from "../../organic/target/wasm32-unknown-unknown/js/wasm";
|
||||
import styles from "./Editor.module.css";
|
||||
|
||||
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
|
||||
`;
|
||||
|
||||
function Editor({ defaultValue = default_org_source }) {
|
||||
interface EditorProps {
|
||||
value: string;
|
||||
setValue: Function;
|
||||
highlights: Highlight[];
|
||||
clearHighlights: Function;
|
||||
}
|
||||
function Editor({
|
||||
value,
|
||||
setValue,
|
||||
highlights,
|
||||
clearHighlights,
|
||||
}: EditorProps): React.ReactNode {
|
||||
function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
setValue(event.target.value);
|
||||
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 shadowRef = useRef<HTMLDivElement>(null);
|
||||
function onTextAreaScroll() {
|
||||
@@ -72,28 +29,26 @@ function Editor({ defaultValue = default_org_source }) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Make sure the text area and shadow div start out in sync.
|
||||
onTextAreaScroll();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="Editor">
|
||||
<div className="Editor-textwrapper">
|
||||
<div className={styles.EditorTextWrapper}>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
onChange={handleChange}
|
||||
className="Editor-textarea"
|
||||
className={styles.EditorTextArea}
|
||||
value={value}
|
||||
onScroll={onTextAreaScroll}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div ref={shadowRef} className="Editor-underlay">
|
||||
<div ref={shadowRef} className={styles.EditorUnderlay}>
|
||||
{buildShadow(highlights, value)}
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<OrgAst
|
||||
setHighlight={setHighlight}
|
||||
clearHighlights={clearHighlights}
|
||||
value={value}
|
||||
astTree={astTree}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
11
src/Explorer.module.css
Normal file
11
src/Explorer.module.css
Normal 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
1
src/Explorer.module.css.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const Explorer: string;
|
||||
82
src/Explorer.tsx
Normal file
82
src/Explorer.tsx
Normal 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;
|
||||
@@ -1,17 +1,57 @@
|
||||
.OrgAst {
|
||||
flex: 1;
|
||||
background: #eeeeee;
|
||||
--ast-background-color: #111111;
|
||||
--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;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.OrgAstNode {
|
||||
border: 1px solid #000000;
|
||||
background: #ffffff;
|
||||
box-shadow: 3px 3px 4px #000000;
|
||||
border: 1px solid var(--ast-node-border-color);
|
||||
background: var(--ast-node-background-color);
|
||||
box-shadow: -3px 3px 1px var(--ast-node-shadow-color);
|
||||
|
||||
> details {
|
||||
border: 1px solid #000000;
|
||||
border: 1px solid var(--ast-node-subsection-border-color);
|
||||
margin: 5px 5px 5px 2px;
|
||||
|
||||
> summary {
|
||||
@@ -24,27 +64,28 @@
|
||||
> summary {
|
||||
border-width: 0 0 1px 0;
|
||||
border-style: dotted;
|
||||
border-color: #000000;
|
||||
border-color: var(--ast-node-subsection-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.OrgAstNode.selected {
|
||||
> .OrgAstNodeType {
|
||||
background: #9cfc97;
|
||||
background: var(--ast-node-type-selected-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.OrgAstNode.hovered:not(.selected) {
|
||||
> .OrgAstNodeType {
|
||||
background: #70f8ba;
|
||||
background: var(--ast-node-type-hover-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.OrgAstNodeType {
|
||||
background: #6ccff6;
|
||||
background: var(--ast-node-type-background-color);
|
||||
padding: 3px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
> span:first-child {
|
||||
font-size: 1.1rem;
|
||||
@@ -64,20 +105,20 @@
|
||||
|
||||
.OrgAstChildren {
|
||||
padding: 5px 5px 5px 20px;
|
||||
background: #eeeeee;
|
||||
background: var(--ast-node-children-background-color);
|
||||
}
|
||||
|
||||
.OrgAstProperties,
|
||||
.OrgAstObjectTree,
|
||||
.OrgAstOptionalPair {
|
||||
border: 1px solid #000000;
|
||||
border: 1px solid var(--ast-node-subsection-border-color);
|
||||
margin: 5px;
|
||||
|
||||
> tbody {
|
||||
> tr {
|
||||
border-width: 1px 0;
|
||||
border-style: solid;
|
||||
border-color: #000000;
|
||||
border-color: var(--ast-node-subsection-border-color);
|
||||
|
||||
> th,
|
||||
> td {
|
||||
@@ -92,10 +133,10 @@
|
||||
}
|
||||
|
||||
> tr:nth-child(odd) {
|
||||
background-color: #eeeeee;
|
||||
background-color: var(--ast-node-table-row-odd-background-color);
|
||||
}
|
||||
> tr:nth-child(even) {
|
||||
background-color: #ffffff;
|
||||
background-color: var(--ast-node-table-row-even-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +144,7 @@
|
||||
.OrgAstObjectTree {
|
||||
> tbody {
|
||||
border-style: dashed;
|
||||
border-color: #000000;
|
||||
border-color: var(--ast-node-subsection-border-color);
|
||||
}
|
||||
> tbody:not(:first-child, :last-child) {
|
||||
border-width: 3px 0;
|
||||
|
||||
@@ -14,10 +14,18 @@ const OrgAst = (props: {
|
||||
const [hoveredNode, setHoveredNode] = useState<OrgNodeReference | null>(null);
|
||||
|
||||
function selectNode(uid: string, start: number, end: number) {
|
||||
if (selectedNode !== null && selectedNode.uid === uid) {
|
||||
props.setHighlight([]);
|
||||
setSelectedNode(null);
|
||||
setHoveredNode(null);
|
||||
} else {
|
||||
const new_node: OrgNodeReference = { uid: uid, start: start, end: end };
|
||||
props.setHighlight([new_node, hoveredNode].filter((node) => node !== null));
|
||||
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 };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { ReactNode, useState } from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { Highlight } from "./highlight";
|
||||
import styles from "./Editor.module.css";
|
||||
|
||||
function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
|
||||
let output: ReactNode[] = [];
|
||||
@@ -22,7 +23,7 @@ function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
|
||||
} else if (state == ShadowState.Highlight && !thisCharHighlighted) {
|
||||
// End the span
|
||||
output.push(
|
||||
<span key={i} className="highlighted">
|
||||
<span key={i} className={styles.EditorHighlighted}>
|
||||
{buffer}
|
||||
</span>,
|
||||
);
|
||||
@@ -37,7 +38,7 @@ function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
|
||||
output.push(buffer);
|
||||
} else if (state == ShadowState.Highlight) {
|
||||
output.push(
|
||||
<span key={i} className="highlighted">
|
||||
<span key={i} className={styles.EditorHighlighted}>
|
||||
{buffer}
|
||||
</span>,
|
||||
);
|
||||
|
||||
@@ -75,8 +75,6 @@ module.exports = {
|
||||
filename: "index.html",
|
||||
}),
|
||||
],
|
||||
mode: "development",
|
||||
devtool: "inline-source-map",
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
7
webpack.dev.js
Normal file
7
webpack.dev.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { merge } = require("webpack-merge");
|
||||
const common = require("./webpack.common.js");
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: "development",
|
||||
devtool: "inline-source-map",
|
||||
});
|
||||
6
webpack.prd.js
Normal file
6
webpack.prd.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { merge } = require("webpack-merge");
|
||||
const common = require("./webpack.common.js");
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: "production",
|
||||
});
|
||||
Reference in New Issue
Block a user