diff --git a/package-lock.json b/package-lock.json index 3ba9e5ba7..8e0fe54ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,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", @@ -19040,6 +19041,15 @@ "react-dom": ">=16.0.0" } }, + "node_modules/react18-json-view": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/react18-json-view/-/react18-json-view-0.2.8.tgz", + "integrity": "sha512-uJlcf5PEDaba6yTqfcDAcMSYECZ15SLcpP94mLFTa/+fa1kZANjERqKzS7YxxsrGP4+jDxt6sIaglR0PbQcKPw==", + "dev": true, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -25829,14 +25839,6 @@ "node": ">=18" } }, - "packages/parser/node_modules/react18-json-view": { - "version": "0.2.8", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, "packages/standalone": { "name": "@doenet/standalone", "version": "0.7.0-alpha17", diff --git a/package.json b/package.json index e7ec21497..9c566706b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/doenetml-prototype/src/editor/components/download-pretext.tsx b/packages/doenetml-prototype/src/editor/components/download-pretext.tsx index 03e95d2f0..6e4ceeb8c 100644 --- a/packages/doenetml-prototype/src/editor/components/download-pretext.tsx +++ b/packages/doenetml-prototype/src/editor/components/download-pretext.tsx @@ -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); @@ -12,6 +17,7 @@ export function DownloadPretextDropdownItem() { onClick={() => { console.log(flatDast); console.log(toXml(doenetToPretext(flatDast))); + console.log(renderToPretext(flatDast)); }} > PreTeXt diff --git a/packages/doenetml-prototype/src/global-config.ts b/packages/doenetml-prototype/src/global-config.ts index e2f667ccb..9d4b728e6 100644 --- a/packages/doenetml-prototype/src/global-config.ts +++ b/packages/doenetml-prototype/src/global-config.ts @@ -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"; + } +} diff --git a/packages/doenetml-prototype/src/renderers/doenet/README.md b/packages/doenetml-prototype/src/renderers/doenet/README.md new file mode 100644 index 000000000..0e0ec548d --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/doenet/README.md @@ -0,0 +1,3 @@ +# Doenet Components + +These components are for rendering interactive Doenet on a webpage \ No newline at end of file diff --git a/packages/doenetml-prototype/src/renderers/components/_fragment.tsx b/packages/doenetml-prototype/src/renderers/doenet/_fragment.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/_fragment.tsx rename to packages/doenetml-prototype/src/renderers/doenet/_fragment.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/answer.tsx b/packages/doenetml-prototype/src/renderers/doenet/answer.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/answer.tsx rename to packages/doenetml-prototype/src/renderers/doenet/answer.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/boolean.tsx b/packages/doenetml-prototype/src/renderers/doenet/boolean.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/boolean.tsx rename to packages/doenetml-prototype/src/renderers/doenet/boolean.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/choice-input.css b/packages/doenetml-prototype/src/renderers/doenet/choice-input.css similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/choice-input.css rename to packages/doenetml-prototype/src/renderers/doenet/choice-input.css diff --git a/packages/doenetml-prototype/src/renderers/components/choice-input.tsx b/packages/doenetml-prototype/src/renderers/doenet/choice-input.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/choice-input.tsx rename to packages/doenetml-prototype/src/renderers/doenet/choice-input.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/division.tsx b/packages/doenetml-prototype/src/renderers/doenet/division.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/division.tsx rename to packages/doenetml-prototype/src/renderers/doenet/division.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/document.tsx b/packages/doenetml-prototype/src/renderers/doenet/document.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/document.tsx rename to packages/doenetml-prototype/src/renderers/doenet/document.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/graph.css b/packages/doenetml-prototype/src/renderers/doenet/graph.css similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/graph.css rename to packages/doenetml-prototype/src/renderers/doenet/graph.css diff --git a/packages/doenetml-prototype/src/renderers/components/graph.tsx b/packages/doenetml-prototype/src/renderers/doenet/graph.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/graph.tsx rename to packages/doenetml-prototype/src/renderers/doenet/graph.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/index.ts b/packages/doenetml-prototype/src/renderers/doenet/index.ts similarity index 93% rename from packages/doenetml-prototype/src/renderers/components/index.ts rename to packages/doenetml-prototype/src/renderers/doenet/index.ts index 5e5840b4d..d2fd4e7a1 100644 --- a/packages/doenetml-prototype/src/renderers/components/index.ts +++ b/packages/doenetml-prototype/src/renderers/doenet/index.ts @@ -17,3 +17,4 @@ export * from "./xref"; export * from "./ol"; export * from "./ul"; export * from "./li"; +export * from "./choice-input"; diff --git a/packages/doenetml-prototype/src/renderers/components/jsxgraph/listeners.ts b/packages/doenetml-prototype/src/renderers/doenet/jsxgraph/listeners.ts similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/jsxgraph/listeners.ts rename to packages/doenetml-prototype/src/renderers/doenet/jsxgraph/listeners.ts diff --git a/packages/doenetml-prototype/src/renderers/components/li.css b/packages/doenetml-prototype/src/renderers/doenet/li.css similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/li.css rename to packages/doenetml-prototype/src/renderers/doenet/li.css diff --git a/packages/doenetml-prototype/src/renderers/components/li.tsx b/packages/doenetml-prototype/src/renderers/doenet/li.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/li.tsx rename to packages/doenetml-prototype/src/renderers/doenet/li.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/line.tsx b/packages/doenetml-prototype/src/renderers/doenet/line.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/line.tsx rename to packages/doenetml-prototype/src/renderers/doenet/line.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/m.tsx b/packages/doenetml-prototype/src/renderers/doenet/m.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/m.tsx rename to packages/doenetml-prototype/src/renderers/doenet/m.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/math.tsx b/packages/doenetml-prototype/src/renderers/doenet/math.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/math.tsx rename to packages/doenetml-prototype/src/renderers/doenet/math.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/ol.tsx b/packages/doenetml-prototype/src/renderers/doenet/ol.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/ol.tsx rename to packages/doenetml-prototype/src/renderers/doenet/ol.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/p.tsx b/packages/doenetml-prototype/src/renderers/doenet/p.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/p.tsx rename to packages/doenetml-prototype/src/renderers/doenet/p.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/point.tsx b/packages/doenetml-prototype/src/renderers/doenet/point.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/point.tsx rename to packages/doenetml-prototype/src/renderers/doenet/point.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/problem.tsx b/packages/doenetml-prototype/src/renderers/doenet/problem.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/problem.tsx rename to packages/doenetml-prototype/src/renderers/doenet/problem.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/text-input.css b/packages/doenetml-prototype/src/renderers/doenet/text-input.css similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/text-input.css rename to packages/doenetml-prototype/src/renderers/doenet/text-input.css diff --git a/packages/doenetml-prototype/src/renderers/components/text-input.tsx b/packages/doenetml-prototype/src/renderers/doenet/text-input.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/text-input.tsx rename to packages/doenetml-prototype/src/renderers/doenet/text-input.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/text.tsx b/packages/doenetml-prototype/src/renderers/doenet/text.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/text.tsx rename to packages/doenetml-prototype/src/renderers/doenet/text.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/title.tsx b/packages/doenetml-prototype/src/renderers/doenet/title.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/title.tsx rename to packages/doenetml-prototype/src/renderers/doenet/title.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/ul.tsx b/packages/doenetml-prototype/src/renderers/doenet/ul.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/ul.tsx rename to packages/doenetml-prototype/src/renderers/doenet/ul.tsx diff --git a/packages/doenetml-prototype/src/renderers/components/xref.tsx b/packages/doenetml-prototype/src/renderers/doenet/xref.tsx similarity index 100% rename from packages/doenetml-prototype/src/renderers/components/xref.tsx rename to packages/doenetml-prototype/src/renderers/doenet/xref.tsx diff --git a/packages/doenetml-prototype/src/renderers/element.tsx b/packages/doenetml-prototype/src/renderers/element.tsx index 4bcc112db..36233fcc2 100644 --- a/packages/doenetml-prototype/src/renderers/element.tsx +++ b/packages/doenetml-prototype/src/renderers/element.tsx @@ -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"); @@ -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 @@ -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 diff --git a/packages/doenetml-prototype/src/renderers/get-component.ts b/packages/doenetml-prototype/src/renderers/get-component.ts index e637b0d5f..6e1f899fe 100644 --- a/packages/doenetml-prototype/src/renderers/get-component.ts +++ b/packages/doenetml-prototype/src/renderers/get-component.ts @@ -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; - passthroughChildren?: false; -} & CommonProps; -type ComponentWithPassthroughChildren = { - component: BasicComponentWithPassthroughChildren; - 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. */ @@ -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" diff --git a/packages/doenetml-prototype/src/renderers/pretext/README.md b/packages/doenetml-prototype/src/renderers/pretext/README.md new file mode 100644 index 000000000..3befae4e2 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/README.md @@ -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). \ No newline at end of file diff --git a/packages/doenetml-prototype/src/renderers/pretext/_fragment.tsx b/packages/doenetml-prototype/src/renderers/pretext/_fragment.tsx new file mode 100644 index 000000000..9039fb6f7 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/_fragment.tsx @@ -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 {children}; +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/_pass-through-with-tag.tsx b/packages/doenetml-prototype/src/renderers/pretext/_pass-through-with-tag.tsx new file mode 100644 index 000000000..fa586e028 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/_pass-through-with-tag.tsx @@ -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 `

`). + */ +export const _PassThroughWithTag: BasicComponentWithPassthroughChildren<{}> = ({ + children, + node, +}) => { + return React.createElement(node.name, node.attributes, children); +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/answer.tsx b/packages/doenetml-prototype/src/renderers/pretext/answer.tsx new file mode 100644 index 000000000..ea47ba6c0 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/answer.tsx @@ -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 ; + } + return ( + + + + + ); +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/boolean.tsx b/packages/doenetml-prototype/src/renderers/pretext/boolean.tsx new file mode 100644 index 000000000..c891d4024 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/boolean.tsx @@ -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 = ({ node }) => { + return {node.data.props.value.toString()}; +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/choice-input.css b/packages/doenetml-prototype/src/renderers/pretext/choice-input.css new file mode 100644 index 000000000..d02c26047 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/choice-input.css @@ -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; + } + } +} diff --git a/packages/doenetml-prototype/src/renderers/pretext/choice-input.tsx b/packages/doenetml-prototype/src/renderers/pretext/choice-input.tsx new file mode 100644 index 000000000..7dc1d7538 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/choice-input.tsx @@ -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 ( + + + {choiceChildren.map((child, i) => ( + + ))} + + + ); +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/division.tsx b/packages/doenetml-prototype/src/renderers/pretext/division.tsx new file mode 100644 index 000000000..ecedcf784 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/division.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { BasicComponentWithPassthroughChildren } from "../types"; +import { Element } from "../element"; +import type { DivisionProps } from "@doenet/doenetml-worker-rust"; +import { generateHtmlId } from "../utils"; + +export const Division: BasicComponentWithPassthroughChildren<{ + props: DivisionProps; +}> = ({ children, node, annotation, ancestors }) => { + const htmlId = generateHtmlId(node, annotation, ancestors); + const titleElmId = node.data.props.title; + const codeNumber = node.data.props.codeNumber; + const xrefLabel = node.data.props.xrefLabel; + const displayName = `${xrefLabel.label}${ + codeNumber ? ` ${codeNumber}.` : "" + }`; + + const title = + titleElmId != null ? ( + + ) : ( + "" + ); + + return ( +

+ + {displayName} {title} + + {children} +
+ ); +}; \ No newline at end of file diff --git a/packages/doenetml-prototype/src/renderers/pretext/graph.css b/packages/doenetml-prototype/src/renderers/pretext/graph.css new file mode 100644 index 000000000..8e8a64357 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/graph.css @@ -0,0 +1,66 @@ +.doenet-document { + .graph-container { + @apply max-w-full aspect-square border-2 border-black rounded-md touch-none select-none w-72 relative; + background-color: var(--canvas); + color: var(--canvastext); + } + .jsxgraph-container { + @apply w-full h-full; + } + + .toolbar { + @apply p-0.5 rounded-md hover:bg-slate-300 hover:bg-opacity-40; + } + + .toolbar.grouped-buttons .button { + @apply first:rounded-s-md last:rounded-e-md; + } + + .jsxgraph-nav-buttons { + @apply flex flex-row justify-between absolute bottom-0 right-0 p-0.5 w-full items-end; + } + + /* styles modified from https://ariakit.org/components/toolbar */ + .button { + @apply p-1 rounded-none dn-button-transparent secondary; + } + + .button[data-focus-visible] { + outline-style: solid; + } + + .pan-buttons { + @apply rounded-full; + display: grid; + grid-template-areas: + ". north ." + "west center east" + ". south ."; + justify-items: center; + align-items: center; + } + + .pan-buttons > .north { + @apply rounded-t-md; + grid-area: north; + } + + .pan-buttons > .south { + @apply rounded-b-md; + grid-area: south; + } + + .pan-buttons > .east { + @apply rounded-r-md; + grid-area: east; + } + + .pan-buttons > .west { + @apply rounded-l-md; + grid-area: west; + } + + .pan-buttons > .center { + grid-area: center; + } +} diff --git a/packages/doenetml-prototype/src/renderers/pretext/graph.tsx b/packages/doenetml-prototype/src/renderers/pretext/graph.tsx new file mode 100644 index 000000000..7c55791c6 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/graph.tsx @@ -0,0 +1,220 @@ +import React from "react"; +import { + VscChevronDown, + VscChevronLeft, + VscChevronRight, + VscChevronUp, + VscScreenNormal, + VscTarget, + VscZoomIn, + VscZoomOut, +} from "react-icons/vsc"; +import * as JSG from "jsxgraph"; +import { JSXGraph } from "jsxgraph"; +import { BasicComponent } from "../types"; +import "./graph.css"; +import { Toolbar, ToolbarItem } from "@ariakit/react"; +import { Element } from "../element"; + +(window as any).JSG = JSG; + +export const GraphContext = React.createContext(null); + +/** + * Which layer each type of element is to be drawn on. These layers determine what shows up on top of what. + * + * NOTE: there can be at most 10 different layer offsets, + * given that the DoenetML layer is multiplied by 10 and added to these offsets + */ +export const LAYER_OFFSETS = { + base: 0, + image: 1, + line: 2, + vertex: 3, + controlPoint: 4, + point: 5, + text: 6, +}; + +export const Graph: BasicComponent = ({ node }) => { + const boardId = "jsxgraph-board-" + node.data.id; + const boardRef = React.useRef(null); + const [board, setBoard] = React.useState(null); + const [xaxis, setXaxis] = React.useState(null); + const [yaxis, setYaxis] = React.useState(null); + + React.useLayoutEffect(() => { + if (!boardRef.current) { + return; + } + const board = JSXGraph.initBoard(boardRef.current, { + axis: false, + grid: false, + showNavigation: false, + showCopyright: false, + boundingBox: [-5, 5, 5, -5], + // Sometimes needed to keep the board from continually expanding + resize: { enabled: false, throttle: 100 }, + }); + + const xaxis = board.create( + "axis", + [ + [0, 0], + [1, 0], + ], + { + ticks: { + visible: true, + majorHeight: 10, + minorHeight: 5, + strokeColor: "var(--canvastext)", + strokeWidth: 1, + }, + highlight: false, + strokeColor: "var(--canvastext)", + }, + ); + setXaxis(xaxis); + const yaxis = board.create( + "axis", + [ + [0, 0], + [0, 1], + ], + { + ticks: { + visible: true, + majorHeight: 10, + minorHeight: 5, + strokeColor: "var(--canvastext)", + strokeWidth: 1, + }, + highlight: false, + strokeColor: "var(--canvastext)", + }, + ); + setYaxis(yaxis); + + setBoard(board); + }, [boardRef]); + + const elementChildrenIds = React.useMemo( + () => + node.children + .filter((n) => typeof n === "object" && "id" in n) + .map((n) => (n as any).id) as number[], + [node.children], + ); + + return ( +
+
+ + {elementChildrenIds.map((id) => ( + + ))} + + +
+ ); +}; + +function NavButtons({ board }: { board: JSG.Board | null }) { + if (!board) { + return "No Board"; + } + return ( +
+ + { + board.zoomOut(); + }} + > + + + { + board.zoom100(); + }} + > + + + { + board.zoomIn(); + }} + > + + + + + { + board.clickLeftArrow(); + }} + > + + + { + board.clickRightArrow(); + }} + > + + + { + board.clickDownArrow(); + }} + > + + + { + board.clickUpArrow(); + }} + > + + + { + const [xmin, ymax, xmax, ymin] = board.getBoundingBox(); + const width = xmax - xmin; + const height = ymax - ymin; + + board.setBoundingBox([ + -width / 2, + height / 2, + width / 2, + -height / 2, + ]); + }} + > + + + +
+ ); +} diff --git a/packages/doenetml-prototype/src/renderers/pretext/index.ts b/packages/doenetml-prototype/src/renderers/pretext/index.ts new file mode 100644 index 000000000..3be9f7633 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/index.ts @@ -0,0 +1,20 @@ +export * from "./p"; +export * from "./m"; +export * from "./math"; +export * from "./graph"; +export * from "./division"; +export * from "./problem"; +export * from "./answer"; +export * from "./text-input"; +export * from "./line"; +export * from "./point"; +export * from "./text"; +export * from "./boolean"; +export * from "./title"; +export * from "./_fragment"; +export * from "./xref"; +export * from "./ol"; +export * from "./ul"; +export * from "./li"; +export * from "./choice-input"; +export * from "./_pass-through-with-tag"; diff --git a/packages/doenetml-prototype/src/renderers/pretext/jsxgraph/listeners.ts b/packages/doenetml-prototype/src/renderers/pretext/jsxgraph/listeners.ts new file mode 100644 index 000000000..c010606e6 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/jsxgraph/listeners.ts @@ -0,0 +1,56 @@ +import * as JSG from "jsxgraph"; + +type InteractionState = { + dragActive: boolean; + pointerAtDown: [number, number]; + pointerIsDown: boolean; +}; + +export function attachStandardGraphListeners( + obj: T, +) { + const interactionState: InteractionState = { + dragActive: false, + pointerAtDown: [0, 0], + pointerIsDown: false, + }; + + obj.on("drag", function (_e) { + const e = _e as MouseEvent; + const viaPointer = e.type === "pointermove"; + + // Protect against very small unintended drags + if ( + !viaPointer || + Math.abs(e.x - interactionState.pointerAtDown[0]) > 0.1 || + Math.abs(e.y - interactionState.pointerAtDown[1]) > 0.1 + ) { + interactionState.dragActive = true; + } + console.log("dragging", obj.elType, { interactionState, obj }); + }); + obj.on("down", function (_e) { + const e = _e as MouseEvent; + interactionState.pointerAtDown = [e.x, e.y]; + console.log("down", obj.elType, { interactionState, obj }); + }); + obj.on("hit", function (_e) { + interactionState.dragActive = false; + console.log("hit", obj.elType, { interactionState, obj }); + }); + obj.on("up", function (_e) { + interactionState.dragActive = false; + console.log("up", obj.elType, { interactionState, obj }); + }); + obj.on("keyfocusout", function (_e) { + interactionState.dragActive = false; + console.log("keyfocusout", obj.elType, { interactionState, obj }); + }); + obj.on("keydown", function (_e) { + const e = _e as KeyboardEvent; + if (e.key === "Enter") { + interactionState.dragActive = false; + } + console.log("keydown", obj.elType, { interactionState, obj }); + }); +} diff --git a/packages/doenetml-prototype/src/renderers/pretext/li.css b/packages/doenetml-prototype/src/renderers/pretext/li.css new file mode 100644 index 000000000..85d613e4f --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/li.css @@ -0,0 +1,9 @@ +.doenet-document { + ol, + ul { + @apply ps-8 relative; + } + .list-label { + @apply w-5 inline-block absolute left-0 text-right select-none; + } +} diff --git a/packages/doenetml-prototype/src/renderers/pretext/li.tsx b/packages/doenetml-prototype/src/renderers/pretext/li.tsx new file mode 100644 index 000000000..ef7a946f1 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/li.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { BasicComponentWithPassthroughChildren } from "../types"; +import type { LiProps } from "@doenet/doenetml-worker-rust"; +import "./li.css"; +import { generateHtmlId } from "../utils"; + +export const Li: BasicComponentWithPassthroughChildren<{ + props: LiProps; +}> = ({ children, node, annotation, ancestors }) => { + const htmlId = generateHtmlId(node, annotation, ancestors); + const label = node.data.props.label; + return ( +
  • + {label} + {children} +
  • + ); +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/line.tsx b/packages/doenetml-prototype/src/renderers/pretext/line.tsx new file mode 100644 index 000000000..481a6af4d --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/line.tsx @@ -0,0 +1,127 @@ +import React from "react"; +import { BasicComponent } from "../types"; +import { GraphContext, LAYER_OFFSETS } from "./graph"; +import * as JSG from "jsxgraph"; +import { attachStandardGraphListeners } from "./jsxgraph/listeners"; + +export const LineInGraph: BasicComponent = ({ node }) => { + const board = React.useContext(GraphContext); + const lineRef = React.useRef(null); + + React.useEffect(() => { + if (!board) { + lineRef.current = null; + return; + } + if (lineRef.current) { + return; + } + const line = createLine(board, { + numericalPoints: [ + [0, 0], + [1, 1], + ], + labelForGraph: "test", + lineColor: "var(--mainGreen)", + hidden: false, + fixed: false, + draggable: true, + fixLocation: false, + layer: 0, + selectedStyle: { lineStyle: "solid", lineOpacity: 1, lineWidth: 2 }, + dashed: false, + }); + lineRef.current = line; + if (!line) { + return; + } + + attachStandardGraphListeners(line); + + return () => { + line.off("drag"); + line.off("down"); + line.off("hit"); + line.off("up"); + line.off("keyfocusout"); + line.off("keydown"); + board.removeObject(line); + }; + }, [board, lineRef]); + + if (!board) { + return null; + } + + return null; +}; + +function createLine( + board: JSG.Board, + props: { + numericalPoints: [[number, number], [number, number]]; + labelForGraph: string; + lineColor: string; + hidden: boolean; + fixed: boolean; + draggable: boolean; + fixLocation: boolean; + layer: number; + selectedStyle: { + lineStyle: string; + lineOpacity: number; + lineWidth: number; + }; + dashed: boolean; + }, +) { + if ( + props.numericalPoints?.length !== 2 || + props.numericalPoints.some((x) => x.length !== 2) + ) { + return null; + } + + const lineColor = props.lineColor; + + // Things to be passed to JSXGraph as attributes + const jsxLineAttributes: JSG.GeometryElementAttributes & + JSG.LineAttributes = { + name: props.labelForGraph, + visible: !props.hidden, + fixed: props.fixed, + layer: 10 * props.layer + LAYER_OFFSETS.line, + strokeColor: lineColor, + strokeOpacity: props.selectedStyle.lineOpacity, + highlightStrokeColor: lineColor, + highlightStrokeOpacity: props.selectedStyle.lineOpacity * 0.5, + strokeWidth: props.selectedStyle.lineWidth, + highlightStrokeWidth: props.selectedStyle.lineWidth, + dash: styleToDash(props.selectedStyle.lineStyle, props.dashed), + highlight: !props.fixLocation, + }; + + const through = [ + [...props.numericalPoints[0]], + [...props.numericalPoints[1]], + ]; + + const line: JSG.Line = board.create("line", through, jsxLineAttributes); + + return line; +} + +/** + * Return the the dash length for a given style. + */ +function styleToDash(style: string, dash: boolean) { + if (style === "dashed" || dash) { + return 2; + } else if (style === "solid") { + return 0; + } else if (style === "dotted") { + return 1; + } else { + return 0; + } +} diff --git a/packages/doenetml-prototype/src/renderers/pretext/m.tsx b/packages/doenetml-prototype/src/renderers/pretext/m.tsx new file mode 100644 index 000000000..f0b366a73 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/m.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { MathJax } from "better-react-mathjax"; +import { BasicComponentWithPassthroughChildren } from "../types"; +import { useAppSelector } from "../../state/hooks"; +import { renderingOnServerSelector } from "../../state/redux-slices/global"; + +export const M: BasicComponentWithPassthroughChildren = ({ children }) => { + const onServer = useAppSelector(renderingOnServerSelector); + if (onServer) { + return {children}; + } + // better-react-mathjax cannot handle multiple children (it will not update when they change) + // so create a single string. + const childrenString = `\\(${ + Array.isArray(children) ? children.join("") : String(children) + }\\)`; + return ( + + {childrenString} + + ); +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/math.tsx b/packages/doenetml-prototype/src/renderers/pretext/math.tsx new file mode 100644 index 000000000..ff0bc79b9 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/math.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { MathJax } from "better-react-mathjax"; +import { BasicComponent } from "../types"; +import { useAppSelector } from "../../state/hooks"; +import { renderingOnServerSelector } from "../../state/redux-slices/global"; + +type MathData = { props: { latex: string } }; + +export const Math: BasicComponent = ({ node }) => { + const onServer = useAppSelector(renderingOnServerSelector); + if (onServer) { + return {node.data.props.latex}; + } + // better-react-mathjax cannot handle multiple children (it will not update when they change) + // so create a single string. + const latexString = `\\(${node.data.props.latex}\\)`; + return ( + + {latexString} + + ); +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/ol.tsx b/packages/doenetml-prototype/src/renderers/pretext/ol.tsx new file mode 100644 index 000000000..0d57d9cf0 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/ol.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { BasicComponentWithPassthroughChildren } from "../types"; +import type { OlProps } from "@doenet/doenetml-worker-rust"; + +export const Ol: BasicComponentWithPassthroughChildren<{ + props: OlProps; +}> = ({ children, node }) => { + return
      {children}
    ; +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/p.tsx b/packages/doenetml-prototype/src/renderers/pretext/p.tsx new file mode 100644 index 000000000..75b0c1d7c --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/p.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { BasicComponentWithPassthroughChildren } from "../types"; +import type { PProps } from "@doenet/doenetml-worker-rust"; + +export const P: BasicComponentWithPassthroughChildren<{ props: PProps }> = ({ + children, +}) => { + return

    {children}

    ; +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/point.tsx b/packages/doenetml-prototype/src/renderers/pretext/point.tsx new file mode 100644 index 000000000..2f7dc3ac8 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/point.tsx @@ -0,0 +1,115 @@ +import React from "react"; +import { BasicComponent } from "../types"; +import { GraphContext, LAYER_OFFSETS } from "./graph"; +import * as JSG from "jsxgraph"; +import { attachStandardGraphListeners } from "./jsxgraph/listeners"; + +export const PointInGraph: BasicComponent = ({ node }) => { + const board = React.useContext(GraphContext); + const pointRef = React.useRef(null); + + React.useEffect(() => { + if (!board) { + pointRef.current = null; + return; + } + if (pointRef.current) { + return; + } + const point = createPoint(board, { + coords: [1, 1], + labelForGraph: "test", + lineColor: "var(--mainPurple)", + hidden: false, + fixed: false, + draggable: true, + fixLocation: false, + layer: 0, + selectedStyle: { lineStyle: "solid", lineOpacity: 1, lineWidth: 2 }, + dashed: false, + }); + pointRef.current = point; + if (!point) { + return; + } + + attachStandardGraphListeners(point); + + return () => { + point.off("drag"); + point.off("down"); + point.off("hit"); + point.off("up"); + point.off("keyfocusout"); + point.off("keydown"); + board.removeObject(point); + }; + }, [board, pointRef]); + + if (!board) { + return null; + } + + return null; +}; + +function createPoint( + board: JSG.Board, + props: { + coords: [number, number]; + labelForGraph: string; + lineColor: string; + hidden: boolean; + fixed: boolean; + draggable: boolean; + fixLocation: boolean; + layer: number; + selectedStyle: { + lineStyle: string; + lineOpacity: number; + lineWidth: number; + }; + dashed: boolean; + }, +) { + const lineColor = props.lineColor; + + // Things to be passed to JSXGraph as attributes + const jsxLineAttributes: JSG.PointAttributes = { + name: props.labelForGraph, + visible: !props.hidden, + fixed: props.fixed, + layer: 10 * props.layer + LAYER_OFFSETS.line, + strokeColor: lineColor, + strokeOpacity: props.selectedStyle.lineOpacity, + highlightStrokeColor: lineColor, + highlightStrokeOpacity: props.selectedStyle.lineOpacity * 0.5, + strokeWidth: props.selectedStyle.lineWidth, + highlightStrokeWidth: props.selectedStyle.lineWidth, + dash: styleToDash(props.selectedStyle.lineStyle, props.dashed), + highlight: !props.fixLocation, + }; + + const line: JSG.Point = board.create( + "point", + props.coords, + jsxLineAttributes, + ); + + return line; +} + +/** + * Return the the dash length for a given style. + */ +function styleToDash(style: string, dash: boolean) { + if (style === "dashed" || dash) { + return 2; + } else if (style === "solid") { + return 0; + } else if (style === "dotted") { + return 1; + } else { + return 0; + } +} diff --git a/packages/doenetml-prototype/src/renderers/pretext/problem.tsx b/packages/doenetml-prototype/src/renderers/pretext/problem.tsx new file mode 100644 index 000000000..eb5335750 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/problem.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { BasicComponentWithPassthroughChildren } from "../types"; +import { Element } from "../element"; + +export const Problem: BasicComponentWithPassthroughChildren<{ + titleElmId?: number; +}> = ({ children, node, visibilityRef }) => { + const titleElmId = node.data.titleElmId; + + const title = titleElmId != null ? : "Problem"; + + return ( +
    +

    {title}

    + {children} +
    + ); +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/text-input.css b/packages/doenetml-prototype/src/renderers/pretext/text-input.css new file mode 100644 index 000000000..77ce752de --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/text-input.css @@ -0,0 +1,10 @@ +.doenet-document { + .text-input { + label { + @apply max-w-full; + } + input { + @apply border-black border-2 px-1 py-0.5 w-24; + } + } +} diff --git a/packages/doenetml-prototype/src/renderers/pretext/text-input.tsx b/packages/doenetml-prototype/src/renderers/pretext/text-input.tsx new file mode 100644 index 000000000..35adb53f2 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/text-input.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import type { Action, TextInputProps } from "@doenet/doenetml-worker-rust"; +import { BasicComponent } from "../types"; +import { useAppDispatch, useAppSelector } from "../../state/hooks"; +import { renderingOnServerSelector } from "../../state/redux-slices/global"; +import "./text-input.css"; +import { coreActions } from "../../state/redux-slices/core"; + +type TextInputData = { props: TextInputProps }; + +export const TextInput: BasicComponent = ({ node }) => { + const onServer = useAppSelector(renderingOnServerSelector); + const id = node.data.id; + const value = node.data.props.immediateValue; + //const disabled = node.data.props.disabled; + const dispatch = useAppDispatch(); + + const updateValue = React.useCallback(() => { + let action: Action = { + component: "textInput", + actionName: "updateValue", + componentIdx: id, + }; + dispatch(coreActions.dispatchAction(action)); + }, [dispatch, value]); + + if (onServer) { + return {value}; + } + + // TODO: change style to gray out when disabled + + return ( + + + + ); +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/text.tsx b/packages/doenetml-prototype/src/renderers/pretext/text.tsx new file mode 100644 index 000000000..b6d72e665 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/text.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { BasicComponent } from "../types"; +import type { TextProps } from "@doenet/doenetml-worker-rust"; + +type TextData = { props: TextProps }; + +export const Text: BasicComponent = ({ node }) => { + return {node.data.props.value}; +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/title.tsx b/packages/doenetml-prototype/src/renderers/pretext/title.tsx new file mode 100644 index 000000000..c992b38d8 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/title.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { BasicComponentWithPassthroughChildren } from "../types"; + +export const Title: BasicComponentWithPassthroughChildren<{}> = ({ + children, +}) => { + return {children}; +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/ul.tsx b/packages/doenetml-prototype/src/renderers/pretext/ul.tsx new file mode 100644 index 000000000..894683d0b --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/ul.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { BasicComponentWithPassthroughChildren } from "../types"; +import type { UlProps } from "@doenet/doenetml-worker-rust"; + +export const Ul: BasicComponentWithPassthroughChildren<{ + props: UlProps; +}> = ({ children, node }) => { + return
      {children}
    ; +}; diff --git a/packages/doenetml-prototype/src/renderers/pretext/xref.tsx b/packages/doenetml-prototype/src/renderers/pretext/xref.tsx new file mode 100644 index 000000000..14624eb7f --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/pretext/xref.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { BasicComponentWithPassthroughChildren } from "../types"; +import { Element } from "../element"; +import type { XrefProps } from "@doenet/doenetml-worker-rust"; + +export const Xref: BasicComponentWithPassthroughChildren<{ + props: XrefProps; +}> = ({ children, node }) => { + const referentHtmlId = `doenet-id-${node.data.props.referent}`; + const label = node.data.props.displayText; + // XXX: when knowls get figured out, make this code generate a knowl in the correct place. + //const referentContent = node.data.props.referentChildren; + //let hasReferentContent = referentContent.length > 0; + // + //if (hasReferentContent) { + // return
    + // + // + // {children} + // {label} + // + // + // {referentContent.map(c => typeof c === "string" ? c : )} + //
    ; + //} + + return ( + + {children} + {label} + + ); +}; diff --git a/packages/doenetml-prototype/src/renderers/renderers.ts b/packages/doenetml-prototype/src/renderers/renderers.ts new file mode 100644 index 000000000..1ebb31075 --- /dev/null +++ b/packages/doenetml-prototype/src/renderers/renderers.ts @@ -0,0 +1,145 @@ +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, + ChoiceInput, +} from "./doenet"; +import * as PretextComponent from "./pretext"; + +export type CommonProps = { + monitorVisibility?: boolean; +}; +export 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; + passthroughChildren?: false; +} & CommonProps; +export type ComponentWithPassthroughChildren = { + component: BasicComponentWithPassthroughChildren; + passthroughChildren: true; +} & CommonProps; + +export type RendererObject = Record< + string, + Component | ComponentWithPassthroughChildren +>; + +/** + * A map of tag names to components. This is used for naive component rendering, where the + * tag name uniquely determines the component to render. + */ +export const TEXT_MODE_COMPONENTS: RendererObject = { + 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 }, +}; + +export const GRAPH_MODE_COMPONENTS: RendererObject = { + line: { component: LineInGraph }, + point: { component: PointInGraph }, +}; + +/** + * A map of tag names to components. This is used for naive component rendering, where the + * tag name uniquely determines the component to render. + */ +export const PRETEXT_TEXT_MODE_COMPONENTS: RendererObject = { + answer: { component: PretextComponent.Answer }, + choiceInput: { component: PretextComponent.ChoiceInput }, + p: { component: PretextComponent.P, passthroughChildren: true }, + document: { + component: PretextComponent._PassThroughWithTag, + passthroughChildren: true, + }, + m: { component: PretextComponent.M, passthroughChildren: true }, + math: { component: PretextComponent.Math }, + graph: { component: PretextComponent.Graph }, + division: { + component: PretextComponent.Division, + passthroughChildren: true, + monitorVisibility: true, + }, + problem: { + component: PretextComponent.Problem, + passthroughChildren: true, + monitorVisibility: true, + }, + textInput: { component: PretextComponent.TextInput }, + text: { component: PretextComponent.Text }, + boolean: { component: PretextComponent.Boolean }, + title: { component: PretextComponent.Title, passthroughChildren: true }, + _fragment: { + component: PretextComponent._Fragment, + passthroughChildren: true, + }, + xref: { component: PretextComponent.Xref, passthroughChildren: true }, + ol: { component: PretextComponent.Ol, passthroughChildren: true }, + ul: { component: PretextComponent.Ul, passthroughChildren: true }, + li: { component: PretextComponent.Li, passthroughChildren: true }, + + // For PreTeXt compatibility + pretext: { + component: PretextComponent._PassThroughWithTag, + passthroughChildren: true, + }, + article: { + component: PretextComponent._PassThroughWithTag, + passthroughChildren: true, + }, + book: { + component: PretextComponent._PassThroughWithTag, + passthroughChildren: true, + }, +}; + +export const PRETEXT_GRAPH_MODE_COMPONENTS: RendererObject = { + line: { component: PretextComponent.LineInGraph }, + point: { component: PretextComponent.PointInGraph }, +}; diff --git a/packages/doenetml-prototype/src/state/redux-slices/global/slice.ts b/packages/doenetml-prototype/src/state/redux-slices/global/slice.ts index 47a834168..4f59b521e 100644 --- a/packages/doenetml-prototype/src/state/redux-slices/global/slice.ts +++ b/packages/doenetml-prototype/src/state/redux-slices/global/slice.ts @@ -10,12 +10,17 @@ export interface GlobalState { * Whether dark mode is the preferred theme. */ darkMode: boolean; + /** + * Whether components should be rendered in Doenet mode (interactive webpage) or PreTeXt mode (static XML for export). + */ + renderMode: "doenet" | "pretext"; } // Define the initial state using that type const initialState: GlobalState = { renderingOnServer: typeof process !== "undefined" && !process?.env?.browser, darkMode: false, + renderMode: "doenet", }; const dastSlice = createSlice({ @@ -25,6 +30,12 @@ const dastSlice = createSlice({ _setDarkMode: (state, action: PayloadAction) => { state.darkMode = action.payload; }, + _setRenderMode: ( + state, + action: PayloadAction, + ) => { + state.renderMode = action.payload; + }, }, }); @@ -38,3 +49,5 @@ export const _globalReducerActions = { ...dastSlice.actions }; const selfSelector = (state: RootState) => state.global; export const renderingOnServerSelector = (state: RootState) => selfSelector(state).renderingOnServer; +export const renderModeSelector = (state: RootState) => + selfSelector(state).renderMode; diff --git a/packages/doenetml-prototype/src/utils/pretext/ensure-pretext-tag.ts b/packages/doenetml-prototype/src/utils/pretext/ensure-pretext-tag.ts new file mode 100644 index 000000000..06be2d0d9 --- /dev/null +++ b/packages/doenetml-prototype/src/utils/pretext/ensure-pretext-tag.ts @@ -0,0 +1,47 @@ +import type { + AnnotatedElementRef, + FlatDastRoot, +} from "@doenet/doenetml-worker-rust"; + +/** + * Mutate `flatDast` to ensure that the root tag is a `` tag with a division tag immediately inside + * (either a `` or a `
    `). + */ +export function ensurePretextTag( + flatDast: FlatDastRoot, + ensuredDivisionType: "article" | "book" = "article", +) { + const elements = flatDast.elements; + // The root of the document is probably a `` tag. If it is, remove it. + if (flatDast.children.length === 1) { + const firstChild = flatDast.children[0]; + if ( + isAnnotatedElementRef(firstChild) && + elements[firstChild.id].name === "document" + ) { + flatDast.children = elements[firstChild.id].children; + } + } + + const hasPretextElement = flatDast.children.find( + (r) => isAnnotatedElementRef(r) && elements[r.id].name === "pretext", + ); + if (!hasPretextElement) { + const id = elements.length; + elements.push({ + type: "element", + name: "pretext", + children: flatDast.children, + attributes: {}, + data: { id }, + }); + flatDast.children = [{ id, annotation: "original" }]; + } + console.log(flatDast) + + // XXX: Finish ensuring the division +} + +function isAnnotatedElementRef(node: any): node is AnnotatedElementRef { + return node?.id != null && node?.type == null; +} diff --git a/packages/doenetml-prototype/src/utils/pretext/render-to-pretext.tsx b/packages/doenetml-prototype/src/utils/pretext/render-to-pretext.tsx new file mode 100644 index 000000000..59acb6190 --- /dev/null +++ b/packages/doenetml-prototype/src/utils/pretext/render-to-pretext.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { + _dastReducerActions, + dastReducer, +} from "../../state/redux-slices/dast"; +import { toXml } from "xast-util-to-xml"; +import { renderToStaticMarkup } from "react-dom/server"; +import { FlatDastRoot } from "../../../../doenetml-worker-rust/dist/CoreWorker"; +import { configureStore } from "@reduxjs/toolkit"; +import { + _globalReducerActions, + globalReducer, +} from "../../state/redux-slices/global"; +import { Provider } from "react-redux"; +import { Element } from "../../renderers"; +import { ensurePretextTag } from "./ensure-pretext-tag"; + +/** + * Use React's rendering pipeline to render the FlatDast to PreTeXt. + */ +export function renderToPretext(flatDast: FlatDastRoot) { + // Do some pre-processing on the root + flatDast = structuredClone(flatDast); + ensurePretextTag(flatDast); + + // Create a new store independent of the existing Redux store. + // We will configure it separately + const store = configureStore({ + reducer: { + dast: dastReducer, + global: globalReducer, + }, + }); + + store.dispatch(_dastReducerActions._setFlatDastRoot(flatDast)); + store.dispatch(_globalReducerActions._setRenderMode("pretext")); + + return renderToStaticMarkup( + + {flatDast.children.map((child) => { + if (typeof child === "string") { + return child; + } + return ( + + ); + })} + , + ); +} diff --git a/packages/doenetml-prototype/test/doenet-to-pretext.test.browser.ts b/packages/doenetml-prototype/test/doenet-to-pretext.test.browser.ts index 8811acf54..1d57c6228 100644 --- a/packages/doenetml-prototype/test/doenet-to-pretext.test.browser.ts +++ b/packages/doenetml-prototype/test/doenet-to-pretext.test.browser.ts @@ -83,4 +83,22 @@ describe("doenet-to-pretext", () => {
    " `); }); + it("expands to pretext element", async () => { + const worker = await workerWithSource(` + + Foo +

    How about foo?

    +
    + `); + const flatDast = flatDastFilterPositionInfo(await worker.returnDast()); + + const pretext = doenetToPretext(flatDast); + expect(toXml(pretext)).toEqual(` +
    +
    Foo + +

    How about foo?

    +
    +
    `); + }); }); diff --git a/packages/doenetml-prototype/test/pretext-export.test.ts b/packages/doenetml-prototype/test/pretext-export.test.ts new file mode 100644 index 000000000..ae648b1ea --- /dev/null +++ b/packages/doenetml-prototype/test/pretext-export.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import util from "util"; +import { lezerToDast, toXml } from "@doenet/parser"; +import { normalizeDocumentDast } from "../src/state/redux-slices/dast/utils/normalize-dast"; +import { ensurePretextTag } from "../src/utils/pretext/ensure-pretext-tag"; +import { FlatDastRoot } from "@doenet/doenetml-worker-rust"; +import { renderToPretext } from "../src/utils/pretext/render-to-pretext"; + +const origLog = console.log; +console.log = (...args) => { + origLog(...args.map((x) => util.inspect(x, false, 10, true))); +}; + +//

    +// Hi +//

    +const SIMPLE_FLAT_DAST = { + type: "root", + children: [ + { + id: 0, + annotation: "original", + }, + ], + elements: [ + { + type: "element", + name: "document", + attributes: {}, + children: [ + { + id: 1, + annotation: "original", + }, + ], + data: { + id: 0, + action_names: [], + props: {}, + }, + }, + { + type: "element", + name: "p", + attributes: {}, + children: ["\n Hi\n"], + data: { + id: 1, + action_names: [], + }, + }, + ], + warnings: [], +} as FlatDastRoot; + +describe("Pretext export", async () => { + it("Wraps root in tag", () => { + const flatDast = structuredClone(SIMPLE_FLAT_DAST); + expect(renderToPretext(flatDast)).toMatchObject(""); + }); +}); diff --git a/packages/lsp-tools/src/doenet-to-pretext/expanders/index.ts b/packages/lsp-tools/src/doenet-to-pretext/expanders/index.ts index ca1bb2323..9e2aecd09 100644 --- a/packages/lsp-tools/src/doenet-to-pretext/expanders/index.ts +++ b/packages/lsp-tools/src/doenet-to-pretext/expanders/index.ts @@ -9,6 +9,7 @@ import type { } from "@doenet/doenetml-worker-rust"; import { visit } from "unist-util-visit"; import { FlatDastElementWithProps } from "../types"; +import { _recursiveToPretext } from ".."; type AllDoenetTagNames = FlatDastElementWithProps["name"]; @@ -16,7 +17,7 @@ type AllDoenetTagNames = FlatDastElementWithProps["name"]; * Expand all DoenetML-specific tags to their PreTeXt equivalents. */ export function expandDoenetElementsToPretext( - xastRoot: XastRoot, + xastRoot: XastRoot | XastRootContent, doenetElements: FlatDastRoot["elements"], ) { visit(xastRoot, "element", (element, index, parent) => { @@ -33,6 +34,17 @@ export function expandDoenetElementsToPretext( break; } case "division": { + const divisionType = doenetElement.data.props.divisionType; + const titleId = doenetElement.data.props.title; + if (titleId != null) { + const titleElement = _recursiveToPretext( + { id: titleId, annotation: "original" }, + doenetElements, + ); + expandDoenetElementsToPretext(titleElement, doenetElements); + element.children.unshift(titleElement); + } + Object.assign(element, { name: divisionType }); break; } case "document": { diff --git a/packages/lsp-tools/src/doenet-to-pretext/index.ts b/packages/lsp-tools/src/doenet-to-pretext/index.ts index 09984b623..3c572a8aa 100644 --- a/packages/lsp-tools/src/doenet-to-pretext/index.ts +++ b/packages/lsp-tools/src/doenet-to-pretext/index.ts @@ -26,7 +26,7 @@ export function doenetToPretext(flatDast: FlatDastRoot): XastRoot { let xmlAst = x( null, flatDast.children.map((child) => - recursiveToPretext(child, flatDast.elements), + _recursiveToPretext(child, flatDast.elements), ), ); @@ -55,7 +55,7 @@ export function doenetToPretext(flatDast: FlatDastRoot): XastRoot { /** * Recursively construct an XML tree from `node`. */ -function recursiveToPretext( +export function _recursiveToPretext( node: FlatDastElementContent, elements: FlatDastRoot["elements"], ): XastElementContent { @@ -68,7 +68,7 @@ function recursiveToPretext( } const children: XastElementContent[] = element.children.map((child) => - recursiveToPretext(child, elements), + _recursiveToPretext(child, elements), ); const attributes = Object.fromEntries( @@ -77,6 +77,9 @@ function recursiveToPretext( value.children.map((c) => textContent(c, elements)).join(""), ]), ); + if (node.annotation !== "original") { + delete attributes["name"]; + } const ret = x(element.name, attributes, children); ret.data ??= {}; Object.assign(ret.data, { doenetId: element.data.id });