Skip to content

Commit

Permalink
Initial work on React exporter for Pretext
Browse files Browse the repository at this point in the history
  • Loading branch information
siefkenj committed Jul 9, 2024
1 parent 357aef5 commit 98946a9
Show file tree
Hide file tree
Showing 68 changed files with 1,417 additions and 106 deletions.
18 changes: 10 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"react-resizable-panels": "^2.0.20",
"react-router": "^6.24.1",
"react-router-dom": "^6.24.1",
"react18-json-view": "^0.2.8",
"rollup-plugin-polyfill-node": "^0.13.0",
"rollup-plugin-visualizer": "^5.12.0",
"source-map-generator": "^0.8.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React from "react";
import DropdownItem from "react-bootstrap/DropdownItem";
import { useAppSelector } from "../../state/hooks";
import { flatDastSelector } from "../../state/redux-slices/dast";
import { doenetToMarkdown, doenetToPretext } from "@doenet/lsp-tools";
import {
_dastReducerActions,
flatDastSelector,
} from "../../state/redux-slices/dast";
import { doenetToPretext } from "@doenet/lsp-tools";
import { toXml } from "xast-util-to-xml";
import { _globalReducerActions } from "../../state/redux-slices/global";
import { renderToPretext } from "../../utils/pretext/render-to-pretext";

export function DownloadPretextDropdownItem() {
const flatDast = useAppSelector(flatDastSelector);
Expand All @@ -12,6 +17,7 @@ export function DownloadPretextDropdownItem() {
onClick={() => {
console.log(flatDast);
console.log(toXml(doenetToPretext(flatDast)));
console.log(renderToPretext(flatDast));
}}
>
PreTeXt
Expand Down
29 changes: 27 additions & 2 deletions packages/doenetml-prototype/src/global-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
if (typeof window === "undefined") {
// @ts-ignore
globalThis.window = globalThis;
}

/**
* Global configuration object for DoenetML.
*/
export const doenetGlobalConfig = {
doenetWorkerUrl: new URL("/doenetml-worker/index.js", window.location.href)
.href,
doenetWorkerUrl: getWorkerUrl(),
};
// We want this to be available in the global scope
(window as any).doenetGlobalConfig = doenetGlobalConfig;

/**
* Attempt to resolve the URL of the doenet worker. This function falls back
* to `doenet.org` if an error is thrown.
* @returns
*/
function getWorkerUrl() {
try {
return new URL(
"/doenetml-worker/index.js",
window?.location?.href || "https://doenet.org",
).href;
} catch (e) {
// `window.location.href` may not be a valid URL. For example, in an iframe it
// could be `about:srcdoc`.
return "https://doenet.org/doenetml-worker/CoreWorker.js";
}
}
3 changes: 3 additions & 0 deletions packages/doenetml-prototype/src/renderers/doenet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Doenet Components

These components are for rendering interactive Doenet on a webpage
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from "./xref";
export * from "./ol";
export * from "./ul";
export * from "./li";
export * from "./choice-input";
16 changes: 14 additions & 2 deletions packages/doenetml-prototype/src/renderers/element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import { elementsArraySelector } from "../state/redux-slices/dast";
import { DastErrorComponent } from "./error";
import { ComponentConstraint, getComponent } from "./get-component";
import { VisibilitySensor } from "./visibility-sensor";
import {
import type {
ElementRefAnnotation,
FlatDastElementContent,
} from "@doenet/doenetml-worker-rust";
import { AncestorChain, extendAncestorChain } from "./utils";
import { renderModeSelector } from "../state/redux-slices/global";
import {
PRETEXT_GRAPH_MODE_COMPONENTS,
PRETEXT_TEXT_MODE_COMPONENTS,
} from "./renderers";

const NO_ELEMENTS = Symbol("NO_ELEMENTS");

Expand All @@ -34,6 +39,7 @@ export const Element = React.memo(
}
return elementsArray[id];
});
const renderMode = useAppSelector(renderModeSelector);

if (value === NO_ELEMENTS) {
// If there are no elements at all, we silently do nothing (we're probably waiting for
Expand All @@ -57,7 +63,13 @@ export const Element = React.memo(
ancestors = "";
}

const Component = getComponent(value, constraint);
const Component =
renderMode === "doenet"
? getComponent(value, constraint)
: getComponent(value, constraint, {
textMode: PRETEXT_TEXT_MODE_COMPONENTS,
graphMode: PRETEXT_GRAPH_MODE_COMPONENTS,
});

// We should render the children and pass them into the component
const children = Component.passthroughChildren
Expand Down
100 changes: 12 additions & 88 deletions packages/doenetml-prototype/src/renderers/get-component.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,12 @@
import { BasicComponent, BasicComponentWithPassthroughChildren } from "./types";
import {
Answer,
Document,
Graph,
LineInGraph,
M,
Math,
P,
PointInGraph,
Problem,
Division,
Text,
TextInput,
Boolean,
Title,
_Fragment,
Xref,
Ol,
Li,
Ul,
} from "./components";
import type { FlatDastElement } from "@doenet/doenetml-worker-rust";
import { ChoiceInput } from "./components/choice-input";
import {
GRAPH_MODE_COMPONENTS,
RendererObject,
TEXT_MODE_COMPONENTS,
} from "./renderers";

export type ComponentConstraint = "text" | "graph" | undefined | null;

type CommonProps = {
monitorVisibility?: boolean;
};
type Component = {
// At this point, we don't care about the type of the data prop
// The component author should make sure it's correct.
// TODO: Can we make this a union of all possible data props?
component: BasicComponent<any>;
passthroughChildren?: false;
} & CommonProps;
type ComponentWithPassthroughChildren = {
component: BasicComponentWithPassthroughChildren<any>;
passthroughChildren: true;
} & CommonProps;

/**
* A map of tag names to components. This is used for naive component rendering, where the
* tag name uniquely determines the component to render.
*/
const TEXT_MODE_COMPONENTS: Record<
string,
Component | ComponentWithPassthroughChildren
> = {
answer: { component: Answer },
choiceInput: { component: ChoiceInput },
p: { component: P, passthroughChildren: true },
document: { component: Document, passthroughChildren: true },
m: { component: M, passthroughChildren: true },
math: { component: Math },
graph: { component: Graph },
division: {
component: Division,
passthroughChildren: true,
monitorVisibility: true,
},
problem: {
component: Problem,
passthroughChildren: true,
monitorVisibility: true,
},
textInput: { component: TextInput },
text: { component: Text },
boolean: { component: Boolean },
title: { component: Title, passthroughChildren: true },
_fragment: { component: _Fragment, passthroughChildren: true },
xref: { component: Xref, passthroughChildren: true },
ol: { component: Ol, passthroughChildren: true },
ul: { component: Ul, passthroughChildren: true },
li: { component: Li, passthroughChildren: true },

// For PreTeXt compatibility
pretext: { component: _Fragment, passthroughChildren: true },
article: { component: _Fragment, passthroughChildren: true },
book: { component: _Fragment, passthroughChildren: true },
};

const GRAPH_MODE_COMPONENTS: Record<
string,
Component | ComponentWithPassthroughChildren
> = {
line: { component: LineInGraph },
point: { component: PointInGraph },
};

/**
* Generate a component that will render nothing but will log a warning to the console.
*/
Expand All @@ -109,8 +27,14 @@ function makeNullComponentWithMessage(message: string) {
export function getComponent(
node: FlatDastElement,
constraint?: ComponentConstraint,
): (typeof TEXT_MODE_COMPONENTS)[keyof typeof TEXT_MODE_COMPONENTS] {
renderers: {
textMode: RendererObject;
graphMode: RendererObject;
} = { textMode: TEXT_MODE_COMPONENTS, graphMode: GRAPH_MODE_COMPONENTS },
): RendererObject[keyof RendererObject] {
constraint = constraint ?? "text";
const TEXT_MODE_COMPONENTS = renderers.textMode;
const GRAPH_MODE_COMPONENTS = renderers.graphMode;

const componentLookup =
constraint === "text"
Expand Down
4 changes: 4 additions & 0 deletions packages/doenetml-prototype/src/renderers/pretext/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Pretext Components

These components are for rendering to PreTeXt XML (not for a webpage, but
for recompilation with the PreTeXt compiler).
12 changes: 12 additions & 0 deletions packages/doenetml-prototype/src/renderers/pretext/_fragment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from "react";
import { BasicComponentWithPassthroughChildren } from "../types";

/**
* A _Fragment component is a special component that cannot be directly authored.
* It is meant to display references to existing content.
*/
export const _Fragment: BasicComponentWithPassthroughChildren<{}> = ({
children,
}) => {
return <React.Fragment>{children}</React.Fragment>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";
import { BasicComponentWithPassthroughChildren } from "../types";

/**
* A _PassThroughWithTag component is a special component that cannot be directly authored.
* It will render `node` in directly as an XML element with the same name (e.g. if `node.name` is "p", it will render `<p>`).
*/
export const _PassThroughWithTag: BasicComponentWithPassthroughChildren<{}> = ({
children,
node,
}) => {
return React.createElement(node.name, node.attributes, children);
};
19 changes: 19 additions & 0 deletions packages/doenetml-prototype/src/renderers/pretext/answer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import { BasicComponent } from "../types";
import { useAppSelector } from "../../state/hooks";
import { renderingOnServerSelector } from "../../state/redux-slices/global";

export const Answer: BasicComponent = ({ node }) => {
const onServer = useAppSelector(renderingOnServerSelector);
if (onServer) {
return <span className="answer-input"></span>;
}
return (
<span className="answer-input">
<input type="text" />
<button type="button" className="answer-submit">
Submit
</button>
</span>
);
};
9 changes: 9 additions & 0 deletions packages/doenetml-prototype/src/renderers/pretext/boolean.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";
import { BasicComponent } from "../types";
import type { BooleanProps } from "@doenet/doenetml-worker-rust";

type BooleanData = { props: BooleanProps };

export const Boolean: BasicComponent<BooleanData> = ({ node }) => {
return <span>{node.data.props.value.toString()}</span>;
};
13 changes: 13 additions & 0 deletions packages/doenetml-prototype/src/renderers/pretext/choice-input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.doenet-document {
.choice-input {
label {
@apply max-w-full block cursor-pointer rounded-md relative;
}
label:focus-within {
@apply dn-outline -outline-offset-1;
}
input[type="radio"] {
@apply me-2 ms-6 appearance-none radio;
}
}
}
39 changes: 39 additions & 0 deletions packages/doenetml-prototype/src/renderers/pretext/choice-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react";
import { BasicComponent } from "../types";
import { useAppSelector } from "../../state/hooks";
import { renderingOnServerSelector } from "../../state/redux-slices/global";
import "./choice-input.css";
import { elementsArraySelector } from "../../state/redux-slices/dast";
import { Radio, RadioGroup, RadioProvider } from "@ariakit/react";
import { flatDastChildrenToReactChildren } from "../element";

export const ChoiceInput: BasicComponent = ({ node }) => {
const childrenIds = React.useMemo(
() =>
node.children.filter(
(n) => typeof n === "number",
) as unknown[] as number[],
[node.children],
);
const elements = useAppSelector(elementsArraySelector);

const choiceChildren = childrenIds.flatMap((id) => {
const choice = elements[id];
return choice.type === "element" && choice.name === "choice"
? choice
: [];
});

return (
<RadioProvider>
<RadioGroup className="choice-input">
{choiceChildren.map((child, i) => (
<label key={child.data.id}>
<Radio value={child.data.id} />
{flatDastChildrenToReactChildren(child.children)}
</label>
))}
</RadioGroup>
</RadioProvider>
);
};
Loading

0 comments on commit 98946a9

Please sign in to comment.