mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): add challenge interactive editor (#61805)
Co-authored-by: sembauke <semboot699@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -324,6 +324,7 @@ exports.createSchemaCustomization = ({ actions }) => {
|
||||
isPrivate: Boolean
|
||||
module: String
|
||||
msTrophyId: String
|
||||
nodules: [Nodule]
|
||||
notes: String
|
||||
order: Int
|
||||
prerequisites: [PrerequisiteChallenge]
|
||||
@@ -462,6 +463,11 @@ exports.createSchemaCustomization = ({ actions }) => {
|
||||
beforeAll: String
|
||||
afterAll: String
|
||||
}
|
||||
|
||||
type Nodule {
|
||||
type: String
|
||||
data: JSON
|
||||
}
|
||||
`;
|
||||
createTypes(typeDefs);
|
||||
};
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
"@babel/preset-react": "7.23.3",
|
||||
"@babel/preset-typescript": "7.23.3",
|
||||
"@babel/standalone": "7.23.7",
|
||||
"@codesandbox/sandpack-react": "2.6.9",
|
||||
"@codesandbox/sandpack-themes": "2.0.21",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||
|
||||
@@ -172,6 +172,18 @@ export interface PrerequisiteChallenge {
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
type Nodule = ParagraphNodule | InteractiveEditorNodule;
|
||||
|
||||
type ParagraphNodule = {
|
||||
type: 'paragraph';
|
||||
data: string;
|
||||
};
|
||||
|
||||
type InteractiveEditorNodule = {
|
||||
type: 'interactiveEditor';
|
||||
data: { ext: Ext; name: string; contents: string }[];
|
||||
};
|
||||
|
||||
export type ChallengeNode = {
|
||||
challenge: {
|
||||
block: string;
|
||||
@@ -184,6 +196,7 @@ export type ChallengeNode = {
|
||||
demoType: 'onClick' | 'onLoad' | null;
|
||||
description: string;
|
||||
challengeFiles: ChallengeFiles;
|
||||
nodules: Nodule[];
|
||||
explanation: string;
|
||||
fields: Fields;
|
||||
fillInTheBlank: FillInTheBlank;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
.interactive-editor-wrapper {
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sp-cm .cm-gutters {
|
||||
color: var(--gray-45);
|
||||
}
|
||||
|
||||
.sp-tab-button:hover {
|
||||
background-color: var(--gray-10);
|
||||
color: var(--gray-90) !important;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Sandpack } from '@codesandbox/sandpack-react';
|
||||
import { freeCodeCampDark } from '@codesandbox/sandpack-themes';
|
||||
import './interactive-editor.css';
|
||||
|
||||
export interface InteractiveFile {
|
||||
ext: string;
|
||||
name: string;
|
||||
contents: string;
|
||||
fileKey?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
files: InteractiveFile[];
|
||||
}
|
||||
|
||||
const InteractiveEditor = ({ files }: Props) => {
|
||||
// Build Sandpack files object
|
||||
// https://github.com/codesandbox/sandpack/tree/main/sandpack-react/src/templates
|
||||
const spFiles = useMemo(() => {
|
||||
const obj = {} as Record<
|
||||
string,
|
||||
{ code: string; active?: boolean; hidden?: boolean }
|
||||
>;
|
||||
files.forEach(file => {
|
||||
const ext = file.ext;
|
||||
let path = '';
|
||||
if (ext === 'html') path = '/index.html';
|
||||
else if (ext === 'css') path = '/styles.css';
|
||||
else if (ext === 'js' || ext === 'ts') path = `/index.${ext}`;
|
||||
else if (ext === 'py')
|
||||
return; // python not supported in sandpack vanilla template
|
||||
else if (ext === 'jsx') path = '/App.jsx';
|
||||
else if (ext === 'tsx') path = '/App.tsx';
|
||||
else path = `/index.${ext}`;
|
||||
// TODO: Consider making active file first file in markdown
|
||||
obj[path] = { code: file.contents, active: path === '/index.html' };
|
||||
});
|
||||
return obj;
|
||||
}, [files]);
|
||||
|
||||
function got(ext: string) {
|
||||
return files.some(f => f.ext === ext);
|
||||
}
|
||||
|
||||
const showConsole = got('js') || got('ts');
|
||||
const freeCodeCampDarkSyntax = {
|
||||
...freeCodeCampDark.syntax,
|
||||
punctuation: '#ffff00',
|
||||
definition: '#e2777a',
|
||||
keyword: '#569cd6'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='interactive-editor-wrapper'>
|
||||
<Sandpack
|
||||
template={
|
||||
got('tsx')
|
||||
? 'react-ts'
|
||||
: got('jsx')
|
||||
? 'react'
|
||||
: got('ts')
|
||||
? 'vanilla-ts'
|
||||
: got('html')
|
||||
? 'static'
|
||||
: 'vanilla'
|
||||
}
|
||||
files={spFiles}
|
||||
theme={{
|
||||
colors: {
|
||||
surface1: '#0a0a23',
|
||||
surface2: '#3b3b4f',
|
||||
surface3: '#2a2a40',
|
||||
hover: '#3b3b4f'
|
||||
},
|
||||
syntax: freeCodeCampDarkSyntax
|
||||
}}
|
||||
options={{
|
||||
editorHeight: 450,
|
||||
editorWidthPercentage: 60,
|
||||
showConsole: showConsole,
|
||||
showConsoleButton: showConsole,
|
||||
layout: got('html') ? 'preview' : 'console',
|
||||
showLineNumbers: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
InteractiveEditor.displayName = 'InteractiveEditor';
|
||||
export default InteractiveEditor;
|
||||
@@ -10,9 +10,11 @@ import { YouTubeEvent } from 'react-youtube';
|
||||
import { ObserveKeys } from 'react-hotkeys';
|
||||
|
||||
// Local Utilities
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||
import ChallengeDescription from '../components/challenge-description';
|
||||
import InteractiveEditor from '../components/interactive-editor';
|
||||
import Hotkeys from '../components/hotkeys';
|
||||
import ChallengeTitle from '../components/challenge-title';
|
||||
import VideoPlayer from '../components/video-player';
|
||||
@@ -69,6 +71,17 @@ interface ShowQuizProps {
|
||||
updateSolutionFormValues: () => void;
|
||||
}
|
||||
|
||||
function renderNodule(nodule: ChallengeNode['challenge']['nodules'][number]) {
|
||||
switch (nodule.type) {
|
||||
case 'paragraph':
|
||||
return <PrismFormatted text={nodule.data} />;
|
||||
case 'interactiveEditor':
|
||||
return <InteractiveEditor files={nodule.data} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const ShowGeneric = ({
|
||||
challengeMounted,
|
||||
data: {
|
||||
@@ -79,6 +92,7 @@ const ShowGeneric = ({
|
||||
block,
|
||||
blockType,
|
||||
description,
|
||||
nodules,
|
||||
explanation,
|
||||
challengeType,
|
||||
fields: { blockName, tests },
|
||||
@@ -228,6 +242,12 @@ const ShowGeneric = ({
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{nodules?.map((nodule, i) => {
|
||||
return (
|
||||
<React.Fragment key={i}>{renderNodule(nodule)}</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||
{videoId && (
|
||||
<>
|
||||
@@ -332,6 +352,10 @@ export const query = graphql`
|
||||
blockType
|
||||
challengeType
|
||||
description
|
||||
nodules {
|
||||
type
|
||||
data
|
||||
}
|
||||
explanation
|
||||
helpCategory
|
||||
instructions
|
||||
|
||||
+31
-1
@@ -5,7 +5,7 @@ challengeType: 19
|
||||
dashedName: what-is-the-canvas-api-and-how-does-it-work
|
||||
---
|
||||
|
||||
# --description--
|
||||
# --interactive--
|
||||
|
||||
The `Canvas` API is a powerful tool that lets you manipulate graphics right inside your JavaScript file. Everything begins with a `canvas` element in HTML. This element serves as a drawing surface that you can manipulate using the instance methods and properties of the `Canvas` API.
|
||||
|
||||
@@ -54,6 +54,19 @@ Once you have the 2D context, you can start drawing on the canvas.
|
||||
|
||||
The `Canvas` API provides several methods and properties for drawing shapes, lines, and text. One of those is the `fillStyle` property, which you can combine with the `fillRect()` method to draw a rectangle or square:
|
||||
|
||||
:::interactive_editor
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="my-canvas" width="400" height="400"></canvas>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```js
|
||||
const canvas = document.getElementById("my-canvas");
|
||||
|
||||
@@ -66,6 +79,8 @@ ctx.fillStyle = "crimson";
|
||||
ctx.fillRect(1, 1, 150, 100);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
`fillRect` takes 4 number values which represent the x axis, y axis, width, and height, respectively.
|
||||
|
||||
There's something on the screen now. You can also draw text or even create an animation. Here's a canvas to represent text:
|
||||
@@ -76,6 +91,19 @@ There's something on the screen now. You can also draw text or even create an an
|
||||
|
||||
To finally draw the text, pass the text into the `fillText()` method as the first argument, followed by the values for the x and y axis:
|
||||
|
||||
:::interactive_editor
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="my-text-canvas" width="300" height="70"></canvas>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```js
|
||||
const textCanvas = document.getElementById("my-text-canvas");
|
||||
|
||||
@@ -91,6 +119,8 @@ textCanvasCtx.fillStyle = "crimson";
|
||||
textCanvasCtx.fillText("Hello HTML Canvas!", 1, 50);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The result in the browser will be the red text `Hello HTML Canvas!`.
|
||||
|
||||
These's much more you can do with the `Canvas` API. For example, you can combine it with `requestAnimationFrame` to create custom animations, visualizations, games, and more.
|
||||
|
||||
@@ -182,6 +182,23 @@ const schema = Joi.object().keys({
|
||||
then: Joi.string()
|
||||
}),
|
||||
challengeFiles: Joi.array().items(fileJoi),
|
||||
// TODO: Consider renaming to something else. Stuff show.tsx knows how to render in order
|
||||
nodules: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
type: Joi.valid('paragraph', 'interactiveEditor').required(),
|
||||
data: Joi.when('type', {
|
||||
is: ['interactiveEditor'],
|
||||
then: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
ext: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
contents: Joi.string().required()
|
||||
})
|
||||
),
|
||||
otherwise: Joi.string().required()
|
||||
})
|
||||
})
|
||||
),
|
||||
hasEditableBoundaries: Joi.boolean(),
|
||||
helpCategory: Joi.valid(
|
||||
'JavaScript',
|
||||
|
||||
Generated
+297
@@ -312,6 +312,12 @@ importers:
|
||||
'@babel/standalone':
|
||||
specifier: 7.23.7
|
||||
version: 7.23.7
|
||||
'@codesandbox/sandpack-react':
|
||||
specifier: 2.6.9
|
||||
version: 2.6.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2)
|
||||
'@codesandbox/sandpack-themes':
|
||||
specifier: 2.0.21
|
||||
version: 2.0.21
|
||||
'@fortawesome/fontawesome-svg-core':
|
||||
specifier: 6.7.1
|
||||
version: 6.7.1
|
||||
@@ -2277,6 +2283,48 @@ packages:
|
||||
'@bundled-es-modules/tough-cookie@0.1.6':
|
||||
resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==}
|
||||
|
||||
'@codemirror/autocomplete@6.19.0':
|
||||
resolution: {integrity: sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==}
|
||||
|
||||
'@codemirror/commands@6.8.1':
|
||||
resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==}
|
||||
|
||||
'@codemirror/lang-css@6.3.1':
|
||||
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
|
||||
|
||||
'@codemirror/lang-html@6.4.10':
|
||||
resolution: {integrity: sha512-h/SceTVsN5r+WE+TVP2g3KDvNoSzbSrtZXCKo4vkKdbfT5t4otuVgngGdFukOO/rwRD2++pCxoh6xD4TEVMkQA==}
|
||||
|
||||
'@codemirror/lang-javascript@6.2.4':
|
||||
resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==}
|
||||
|
||||
'@codemirror/language@6.11.3':
|
||||
resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
|
||||
|
||||
'@codemirror/lint@6.8.5':
|
||||
resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==}
|
||||
|
||||
'@codemirror/state@6.5.2':
|
||||
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
|
||||
|
||||
'@codemirror/view@6.38.4':
|
||||
resolution: {integrity: sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==}
|
||||
|
||||
'@codesandbox/nodebox@0.1.8':
|
||||
resolution: {integrity: sha512-2VRS6JDSk+M+pg56GA6CryyUSGPjBEe8Pnae0QL3jJF1mJZJVMDKr93gJRtBbLkfZN6LD/DwMtf+2L0bpWrjqg==}
|
||||
|
||||
'@codesandbox/sandpack-client@2.19.8':
|
||||
resolution: {integrity: sha512-CMV4nr1zgKzVpx4I3FYvGRM5YT0VaQhALMW9vy4wZRhEyWAtJITQIqZzrTGWqB1JvV7V72dVEUCUPLfYz5hgJQ==}
|
||||
|
||||
'@codesandbox/sandpack-react@2.6.9':
|
||||
resolution: {integrity: sha512-JAbpc1emb9lGdZ0zfnfQnJmU91IcH1AUOmoVevB2qwdrxeaQWy5DyKyqRaQDcMyPicXSXMUF6nvDhb0HY34ofw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
react-dom: ^16.8.0 || ^17 || ^18
|
||||
|
||||
'@codesandbox/sandpack-themes@2.0.21':
|
||||
resolution: {integrity: sha512-CMH/MO/dh6foPYb/3eSn2Cu/J3+1+/81Fsaj7VggICkCrmRk0qG5dmgjGAearPTnRkOGORIPHuRqwNXgw0E6YQ==}
|
||||
|
||||
'@csstools/color-helpers@5.0.2':
|
||||
resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3200,6 +3248,24 @@ packages:
|
||||
'@keyv/serialize@1.0.3':
|
||||
resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==}
|
||||
|
||||
'@lezer/common@1.2.3':
|
||||
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
||||
|
||||
'@lezer/css@1.3.0':
|
||||
resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==}
|
||||
|
||||
'@lezer/highlight@1.2.1':
|
||||
resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
|
||||
|
||||
'@lezer/html@1.3.12':
|
||||
resolution: {integrity: sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==}
|
||||
|
||||
'@lezer/javascript@1.5.4':
|
||||
resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
|
||||
|
||||
'@lezer/lr@1.4.2':
|
||||
resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
|
||||
|
||||
'@lmdb/lmdb-darwin-arm64@2.5.3':
|
||||
resolution: {integrity: sha512-RXwGZ/0eCqtCY8FLTM/koR60w+MXyvBUpToXiIyjOcBnC81tAlTUHrRUavCEWPI9zc9VgvpK3+cbumPyR8BSuA==}
|
||||
cpu: [arm64]
|
||||
@@ -3240,6 +3306,9 @@ packages:
|
||||
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@mdx-js/util@2.0.0-next.8':
|
||||
resolution: {integrity: sha512-T0BcXmNzEunFkuxrO8BFw44htvTPuAoKbLvTG41otyZBDV1Rs+JMddcUuaP5vXpTWtgD3grhcrPEwyx88RUumQ==}
|
||||
|
||||
@@ -3759,6 +3828,16 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@react-hook/intersection-observer@3.1.2':
|
||||
resolution: {integrity: sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
|
||||
'@react-hook/passive-layout-effect@1.2.1':
|
||||
resolution: {integrity: sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
|
||||
'@redux-devtools/extension@3.3.0':
|
||||
resolution: {integrity: sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==}
|
||||
peerDependencies:
|
||||
@@ -4269,6 +4348,9 @@ packages:
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@stitches/core@1.2.8':
|
||||
resolution: {integrity: sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==}
|
||||
|
||||
'@stripe/react-stripe-js@1.16.5':
|
||||
resolution: {integrity: sha512-lVPW3IfwdacyS22pP+nBB6/GNFRRhT/4jfgAK6T2guQmtzPwJV1DogiGGaBNhiKtSY18+yS8KlHSu+PvZNclvQ==}
|
||||
peerDependencies:
|
||||
@@ -6208,6 +6290,9 @@ packages:
|
||||
classnames@2.3.2:
|
||||
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
|
||||
|
||||
clean-set@1.1.2:
|
||||
resolution: {integrity: sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug==}
|
||||
|
||||
clean-stack@2.2.0:
|
||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -6271,6 +6356,9 @@ packages:
|
||||
codemirror@5.65.16:
|
||||
resolution: {integrity: sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg==}
|
||||
|
||||
codesandbox-import-util-types@2.2.3:
|
||||
resolution: {integrity: sha512-Qj00p60oNExthP2oR3vvXmUGjukij+rxJGuiaKM6tyUmSyimdZsqHI/TUvFFClAffk9s7hxGnQgWQ8KCce27qQ==}
|
||||
|
||||
collapse-white-space@1.0.6:
|
||||
resolution: {integrity: sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==}
|
||||
|
||||
@@ -6517,6 +6605,9 @@ packages:
|
||||
create-require@1.1.1:
|
||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
|
||||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
cross-fetch@3.1.4:
|
||||
resolution: {integrity: sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==}
|
||||
|
||||
@@ -7223,6 +7314,9 @@ packages:
|
||||
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
escape-carriage@1.3.1:
|
||||
resolution: {integrity: sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==}
|
||||
|
||||
escape-goat@2.1.1:
|
||||
resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -8778,6 +8872,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
intersection-observer@0.10.0:
|
||||
resolution: {integrity: sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==}
|
||||
|
||||
invariant@2.2.4:
|
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||
|
||||
@@ -10663,6 +10760,9 @@ packages:
|
||||
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
outvariant@1.4.0:
|
||||
resolution: {integrity: sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==}
|
||||
|
||||
outvariant@1.4.3:
|
||||
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
|
||||
|
||||
@@ -11545,6 +11645,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
react-devtools-inline@4.4.0:
|
||||
resolution: {integrity: sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ==}
|
||||
|
||||
react-dom@17.0.2:
|
||||
resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==}
|
||||
peerDependencies:
|
||||
@@ -12554,6 +12657,9 @@ packages:
|
||||
state-toggle@1.0.3:
|
||||
resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==}
|
||||
|
||||
static-browser-server@1.0.3:
|
||||
resolution: {integrity: sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA==}
|
||||
|
||||
static-extend@0.1.2:
|
||||
resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -12598,6 +12704,9 @@ packages:
|
||||
streamx@2.22.1:
|
||||
resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==}
|
||||
|
||||
strict-event-emitter@0.4.6:
|
||||
resolution: {integrity: sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==}
|
||||
|
||||
strict-event-emitter@0.5.1:
|
||||
resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
|
||||
|
||||
@@ -12754,6 +12863,9 @@ packages:
|
||||
peerDependencies:
|
||||
webpack: ^4.0.0 || ^5.0.0
|
||||
|
||||
style-mod@4.1.2:
|
||||
resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
|
||||
|
||||
style-to-object@0.3.0:
|
||||
resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==}
|
||||
|
||||
@@ -13703,6 +13815,9 @@ packages:
|
||||
resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
|
||||
deprecated: Use your platform's native performance.now() and performance.timeOrigin.
|
||||
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
w3c-xmlserializer@2.0.0:
|
||||
resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -16596,6 +16711,117 @@ snapshots:
|
||||
'@types/tough-cookie': 4.0.5
|
||||
tough-cookie: 4.1.4
|
||||
|
||||
'@codemirror/autocomplete@6.19.0':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.4
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@codemirror/commands@6.8.1':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.4
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@codemirror/lang-css@6.3.1':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.19.0
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/css': 1.3.0
|
||||
|
||||
'@codemirror/lang-html@6.4.10':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.19.0
|
||||
'@codemirror/lang-css': 6.3.1
|
||||
'@codemirror/lang-javascript': 6.2.4
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.4
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/css': 1.3.0
|
||||
'@lezer/html': 1.3.12
|
||||
|
||||
'@codemirror/lang-javascript@6.2.4':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.19.0
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/lint': 6.8.5
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.4
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/javascript': 1.5.4
|
||||
|
||||
'@codemirror/language@6.11.3':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.4
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
style-mod: 4.1.2
|
||||
|
||||
'@codemirror/lint@6.8.5':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.4
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/state@6.5.2':
|
||||
dependencies:
|
||||
'@marijn/find-cluster-break': 1.0.2
|
||||
|
||||
'@codemirror/view@6.38.4':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
crelt: 1.0.6
|
||||
style-mod: 4.1.2
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
'@codesandbox/nodebox@0.1.8':
|
||||
dependencies:
|
||||
outvariant: 1.4.3
|
||||
strict-event-emitter: 0.4.6
|
||||
|
||||
'@codesandbox/sandpack-client@2.19.8':
|
||||
dependencies:
|
||||
'@codesandbox/nodebox': 0.1.8
|
||||
buffer: 6.0.3
|
||||
dequal: 2.0.3
|
||||
mime-db: 1.54.0
|
||||
outvariant: 1.4.0
|
||||
static-browser-server: 1.0.3
|
||||
|
||||
'@codesandbox/sandpack-react@2.6.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2)':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.19.0
|
||||
'@codemirror/commands': 6.8.1
|
||||
'@codemirror/lang-css': 6.3.1
|
||||
'@codemirror/lang-html': 6.4.10
|
||||
'@codemirror/lang-javascript': 6.2.4
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.4
|
||||
'@codesandbox/sandpack-client': 2.19.8
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@react-hook/intersection-observer': 3.1.2(react@17.0.2)
|
||||
'@stitches/core': 1.2.8
|
||||
anser: 2.1.1
|
||||
clean-set: 1.1.2
|
||||
codesandbox-import-util-types: 2.2.3
|
||||
dequal: 2.0.3
|
||||
escape-carriage: 1.3.1
|
||||
lz-string: 1.5.0
|
||||
react: 17.0.2
|
||||
react-devtools-inline: 4.4.0
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
react-is: 17.0.2
|
||||
|
||||
'@codesandbox/sandpack-themes@2.0.21': {}
|
||||
|
||||
'@csstools/color-helpers@5.0.2': {}
|
||||
|
||||
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
|
||||
@@ -17437,6 +17663,34 @@ snapshots:
|
||||
dependencies:
|
||||
buffer: 6.0.3
|
||||
|
||||
'@lezer/common@1.2.3': {}
|
||||
|
||||
'@lezer/css@1.3.0':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
|
||||
'@lezer/highlight@1.2.1':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@lezer/html@1.3.12':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
|
||||
'@lezer/javascript@1.5.4':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
|
||||
'@lezer/lr@1.4.2':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@lmdb/lmdb-darwin-arm64@2.5.3':
|
||||
optional: true
|
||||
|
||||
@@ -17464,6 +17718,8 @@ snapshots:
|
||||
|
||||
'@lukeed/ms@2.0.2': {}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@mdx-js/util@2.0.0-next.8': {}
|
||||
|
||||
'@microsoft/fetch-event-source@2.0.1': {}
|
||||
@@ -18028,6 +18284,16 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 17.0.83
|
||||
|
||||
'@react-hook/intersection-observer@3.1.2(react@17.0.2)':
|
||||
dependencies:
|
||||
'@react-hook/passive-layout-effect': 1.2.1(react@17.0.2)
|
||||
intersection-observer: 0.10.0
|
||||
react: 17.0.2
|
||||
|
||||
'@react-hook/passive-layout-effect@1.2.1(react@17.0.2)':
|
||||
dependencies:
|
||||
react: 17.0.2
|
||||
|
||||
'@redux-devtools/extension@3.3.0(redux@4.2.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.9
|
||||
@@ -18765,6 +19031,8 @@ snapshots:
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@stitches/core@1.2.8': {}
|
||||
|
||||
'@stripe/react-stripe-js@1.16.5(@stripe/stripe-js@1.54.2)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)':
|
||||
dependencies:
|
||||
'@stripe/stripe-js': 1.54.2
|
||||
@@ -21241,6 +21509,8 @@ snapshots:
|
||||
|
||||
classnames@2.3.2: {}
|
||||
|
||||
clean-set@1.1.2: {}
|
||||
|
||||
clean-stack@2.2.0: {}
|
||||
|
||||
cli-boxes@2.2.1: {}
|
||||
@@ -21304,6 +21574,8 @@ snapshots:
|
||||
|
||||
codemirror@5.65.16: {}
|
||||
|
||||
codesandbox-import-util-types@2.2.3: {}
|
||||
|
||||
collapse-white-space@1.0.6: {}
|
||||
|
||||
collection-visit@1.0.0:
|
||||
@@ -21581,6 +21853,8 @@ snapshots:
|
||||
|
||||
create-require@1.1.1: {}
|
||||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
cross-fetch@3.1.4:
|
||||
dependencies:
|
||||
node-fetch: 2.6.1
|
||||
@@ -22564,6 +22838,8 @@ snapshots:
|
||||
|
||||
escalade@3.1.2: {}
|
||||
|
||||
escape-carriage@1.3.1: {}
|
||||
|
||||
escape-goat@2.1.1: {}
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
@@ -24948,6 +25224,8 @@ snapshots:
|
||||
|
||||
interpret@2.2.0: {}
|
||||
|
||||
intersection-observer@0.10.0: {}
|
||||
|
||||
invariant@2.2.4:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -27246,6 +27524,8 @@ snapshots:
|
||||
|
||||
os-tmpdir@1.0.2: {}
|
||||
|
||||
outvariant@1.4.0: {}
|
||||
|
||||
outvariant@1.4.3: {}
|
||||
|
||||
own-keys@1.0.1:
|
||||
@@ -28233,6 +28513,10 @@ snapshots:
|
||||
- supports-color
|
||||
- vue-template-compiler
|
||||
|
||||
react-devtools-inline@4.4.0:
|
||||
dependencies:
|
||||
es6-symbol: 3.1.3
|
||||
|
||||
react-dom@17.0.2(react@17.0.2):
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -29509,6 +29793,13 @@ snapshots:
|
||||
|
||||
state-toggle@1.0.3: {}
|
||||
|
||||
static-browser-server@1.0.3:
|
||||
dependencies:
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
dotenv: 16.4.5
|
||||
mime-db: 1.54.0
|
||||
outvariant: 1.4.3
|
||||
|
||||
static-extend@0.1.2:
|
||||
dependencies:
|
||||
define-property: 0.2.5
|
||||
@@ -29566,6 +29857,8 @@ snapshots:
|
||||
bare-events: 2.5.4
|
||||
optional: true
|
||||
|
||||
strict-event-emitter@0.4.6: {}
|
||||
|
||||
strict-event-emitter@0.5.1: {}
|
||||
|
||||
strict-uri-encode@2.0.0: {}
|
||||
@@ -29759,6 +30052,8 @@ snapshots:
|
||||
schema-utils: 3.3.0
|
||||
webpack: 5.90.3(webpack-cli@4.10.0)
|
||||
|
||||
style-mod@4.1.2: {}
|
||||
|
||||
style-to-object@0.3.0:
|
||||
dependencies:
|
||||
inline-style-parser: 0.1.1
|
||||
@@ -31007,6 +31302,8 @@ snapshots:
|
||||
dependencies:
|
||||
browser-process-hrtime: 1.0.0
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
w3c-xmlserializer@2.0.0:
|
||||
dependencies:
|
||||
xml-name-validator: 3.0.0
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# --description--
|
||||
|
||||
This is a test file for empty interactive elements.
|
||||
|
||||
# --interactive--
|
||||
|
||||
no subsections
|
||||
@@ -0,0 +1,17 @@
|
||||
# --interactive--
|
||||
|
||||
Normal markdown
|
||||
|
||||
```html
|
||||
<div>This is NOT an interactive element</div>
|
||||
```
|
||||
|
||||
:::interactive_editor
|
||||
|
||||
```js
|
||||
console.log('Interactive JS');
|
||||
```
|
||||
|
||||
non-code md is not allowed
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,43 @@
|
||||
# --interactive--
|
||||
|
||||
Normal markdown
|
||||
|
||||
```html
|
||||
<div>This is NOT an interactive element</div>
|
||||
```
|
||||
|
||||
:::interactive_editor
|
||||
|
||||
```js
|
||||
console.log('Interactive JS');
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::interactive_editor
|
||||
|
||||
```html
|
||||
<div>This is an interactive element</div>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
```html
|
||||
<div>This is not interactive</div>
|
||||
```
|
||||
|
||||
:::interactive_editor
|
||||
|
||||
```html
|
||||
<div>This is an interactive element</div>
|
||||
```
|
||||
|
||||
```js
|
||||
console.log('Interactive JS');
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
```html
|
||||
<div>This is also not interactive</div>
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# --interactive--
|
||||
|
||||
Testing multiple JavaScript files with unique filekeys.
|
||||
|
||||
:::interactive_editor
|
||||
|
||||
```js
|
||||
console.log('First JavaScript file');
|
||||
```
|
||||
|
||||
```js
|
||||
console.log('Second JavaScript file');
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,33 @@
|
||||
# --description--
|
||||
|
||||
This is the main description.
|
||||
|
||||
# --instructions--
|
||||
|
||||
These are the main instructions at depth 1.
|
||||
|
||||
```html
|
||||
<div>Main instructions code</div>
|
||||
```
|
||||
|
||||
# --something-else--
|
||||
|
||||
## --instructions--
|
||||
|
||||
These are nested instructions at depth 2 that should be ignored.
|
||||
|
||||
```html
|
||||
<div>Nested instructions code</div>
|
||||
```
|
||||
|
||||
### --instructions--
|
||||
|
||||
These are nested instructions at depth 3 that should also be ignored.
|
||||
|
||||
# --hints--
|
||||
|
||||
First hint
|
||||
|
||||
```js
|
||||
// test code
|
||||
```
|
||||
@@ -18,6 +18,7 @@ const restoreDirectives = require('./plugins/restore-directives');
|
||||
const tableAndStrikeThrough = require('./plugins/table-and-strikethrough');
|
||||
const addScene = require('./plugins/add-scene');
|
||||
const addQuizzes = require('./plugins/add-quizzes');
|
||||
const addInteractiveElements = require('./plugins/add-interactive-elements');
|
||||
|
||||
// by convention, anything that adds to file.data has the name add<name>.
|
||||
const processor = unified()
|
||||
@@ -47,6 +48,7 @@ const processor = unified()
|
||||
// about.
|
||||
.use(addSeed)
|
||||
.use(addSolution)
|
||||
.use(addInteractiveElements)
|
||||
// the directives will have been parsed and used by this point, any remaining
|
||||
// 'directives' will be from text like the css selector :root. These should be
|
||||
// converted back to text before they're added to the challenge object.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
const { root } = require('mdast-builder');
|
||||
const find = require('unist-util-find');
|
||||
const { isEmpty } = require('lodash');
|
||||
|
||||
const { getFilenames } = require('./utils/get-file-visitor');
|
||||
const { getSection, isMarker } = require('./utils/get-section');
|
||||
const mdastToHTML = require('./utils/mdast-to-html');
|
||||
|
||||
function plugin() {
|
||||
return transformer;
|
||||
|
||||
function transformer(tree, file) {
|
||||
const interactiveNodes = getSection(tree, `--interactive--`, 1);
|
||||
const subSection = find(root(interactiveNodes), isMarker);
|
||||
if (subSection) {
|
||||
throw Error(
|
||||
`The --interactive-- section should not have any subsections. Found subsection ${subSection.children[0].value}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isEmpty(interactiveNodes)) {
|
||||
const nodules =
|
||||
interactiveNodes.map(node => {
|
||||
if (
|
||||
node.type === 'containerDirective' &&
|
||||
node.name === 'interactive_editor'
|
||||
) {
|
||||
return {
|
||||
type: 'interactiveEditor',
|
||||
data: getFiles(node.children)
|
||||
};
|
||||
} else {
|
||||
const paragraph = mdastToHTML([node]);
|
||||
return {
|
||||
type: 'paragraph',
|
||||
data: paragraph
|
||||
};
|
||||
}
|
||||
}) ?? [];
|
||||
|
||||
file.data.nodules = nodules;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFiles(filesNodes) {
|
||||
const invalidNode = filesNodes.find(node => node.type !== 'code');
|
||||
if (invalidNode) {
|
||||
throw Error('The :::interactive_editor should only contain code blocks.');
|
||||
}
|
||||
|
||||
// TODO: refactor into two steps, 1) count languages, 2) map to files
|
||||
const counts = {};
|
||||
|
||||
return filesNodes.map(node => {
|
||||
counts[node.lang] = counts[node.lang] ? counts[node.lang] + 1 : 1;
|
||||
const out = {
|
||||
contents: node.value,
|
||||
ext: node.lang,
|
||||
name:
|
||||
getFilenames(node.lang) +
|
||||
(counts[node.lang] ? `-${counts[node.lang]}` : '')
|
||||
};
|
||||
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = plugin;
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, beforeEach, it, expect } from 'vitest';
|
||||
const parseFixture = require('./../__fixtures__/parse-fixture');
|
||||
const addInteractiveElements = require('./add-interactive-elements');
|
||||
|
||||
describe('add-interactive-editor plugin', () => {
|
||||
const plugin = addInteractiveElements();
|
||||
let file = { data: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
file = { data: {} };
|
||||
});
|
||||
|
||||
it('returns a function', () => {
|
||||
expect(typeof plugin).toEqual('function');
|
||||
});
|
||||
|
||||
it('adds a `nodules` property to `file.data`', async () => {
|
||||
const mockAST = await parseFixture('with-interactive.md');
|
||||
plugin(mockAST, file);
|
||||
expect(file.data).toHaveProperty('nodules');
|
||||
expect(Array.isArray(file.data.nodules)).toBe(true);
|
||||
});
|
||||
|
||||
it('populates `nodules` with editor objects', async () => {
|
||||
const mockAST = await parseFixture('with-interactive.md');
|
||||
plugin(mockAST, file);
|
||||
const editorElements = file.data.nodules.filter(
|
||||
element => element.type === 'interactiveEditor'
|
||||
);
|
||||
|
||||
expect(editorElements).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
data: [
|
||||
{
|
||||
ext: expect.any(String),
|
||||
name: expect.any(String),
|
||||
contents: expect.stringContaining(
|
||||
'<div>This is an interactive element</div>'
|
||||
)
|
||||
}
|
||||
],
|
||||
type: 'interactiveEditor'
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
expect(editorElements).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
data: [
|
||||
{
|
||||
ext: expect.any(String),
|
||||
name: expect.any(String),
|
||||
contents: expect.stringContaining(
|
||||
'This is an interactive element'
|
||||
)
|
||||
}
|
||||
],
|
||||
type: 'interactiveEditor'
|
||||
},
|
||||
{
|
||||
data: [
|
||||
{
|
||||
ext: expect.any(String),
|
||||
name: expect.any(String),
|
||||
contents: expect.stringContaining(
|
||||
"console.log('Interactive JS');"
|
||||
)
|
||||
}
|
||||
],
|
||||
type: 'interactiveEditor'
|
||||
}
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('provides unique names for each file with the same extension', async () => {
|
||||
const mockAST = await parseFixture('with-multiple-js-files.md');
|
||||
plugin(mockAST, file);
|
||||
const editorElements = file.data.nodules.filter(
|
||||
element => element.type === 'interactiveEditor'
|
||||
);
|
||||
|
||||
expect(editorElements).toHaveLength(1);
|
||||
|
||||
const files = editorElements[0].data;
|
||||
expect(files).toHaveLength(2);
|
||||
|
||||
// Both files should be JavaScript but have unique names
|
||||
expect(files[0].ext).toBe('js');
|
||||
expect(files[1].ext).toBe('js');
|
||||
// TODO: only number if there are multiple files.
|
||||
expect(files[0].name).toBe('script-1');
|
||||
expect(files[1].name).toBe('script-2');
|
||||
|
||||
// Contents should match
|
||||
expect(files[0].contents).toBe("console.log('First JavaScript file');");
|
||||
expect(files[1].contents).toBe("console.log('Second JavaScript file');");
|
||||
});
|
||||
|
||||
it('respects the order of elements in the original markdown', async () => {
|
||||
const expectedTypes = [
|
||||
'paragraph',
|
||||
'paragraph',
|
||||
'interactiveEditor',
|
||||
'interactiveEditor',
|
||||
'paragraph',
|
||||
'interactiveEditor',
|
||||
'paragraph'
|
||||
];
|
||||
|
||||
const mockAST = await parseFixture('with-interactive.md');
|
||||
plugin(mockAST, file);
|
||||
const elements = file.data.nodules;
|
||||
const types = elements.map(element => element.type);
|
||||
|
||||
expect(types).toEqual(expectedTypes);
|
||||
});
|
||||
|
||||
it('throws if the interactive_editor directive contains non-code nodes', async () => {
|
||||
const mockAST = await parseFixture('with-interactive-non-code.md');
|
||||
expect(() => plugin(mockAST, file)).toThrow(
|
||||
'The :::interactive_editor should only contain code blocks.'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -10,16 +10,14 @@ function addText(sectionIds) {
|
||||
}
|
||||
function transformer(tree, file) {
|
||||
for (const sectionId of sectionIds) {
|
||||
const textNodes = getSection(tree, `--${sectionId}--`);
|
||||
const textNodes = getSection(tree, `--${sectionId}--`, 1);
|
||||
const subSection = find(root(textNodes), isMarker);
|
||||
if (subSection) {
|
||||
throw Error(
|
||||
`The --${sectionId}-- section should not have any subsections. Found subsection ${subSection.children[0].value}`
|
||||
);
|
||||
}
|
||||
|
||||
const sectionText = mdastToHTML(textNodes);
|
||||
|
||||
if (!isEmpty(sectionText)) {
|
||||
file.data = {
|
||||
...file.data,
|
||||
|
||||
@@ -3,7 +3,7 @@ import parseFixture from '../__fixtures__/parse-fixture';
|
||||
import addText from './add-text';
|
||||
|
||||
describe('add-text', () => {
|
||||
let realisticAST, mockAST, withSubSectionAST;
|
||||
let realisticAST, mockAST, withSubSectionAST, withNestedInstructionsAST;
|
||||
const descriptionId = 'description';
|
||||
const instructionsId = 'instructions';
|
||||
const missingId = 'missing';
|
||||
@@ -13,6 +13,9 @@ describe('add-text', () => {
|
||||
realisticAST = await parseFixture('realistic.md');
|
||||
mockAST = await parseFixture('simple.md');
|
||||
withSubSectionAST = await parseFixture('with-subsection.md');
|
||||
withNestedInstructionsAST = await parseFixture(
|
||||
'with-nested-instructions.md'
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -134,6 +137,20 @@ describe('add-text', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore --instructions-- markers that are not at depth 1', () => {
|
||||
const plugin = addText([instructionsId]);
|
||||
plugin(withNestedInstructionsAST, file);
|
||||
|
||||
// Should only include the depth 1 instructions, not the nested ones
|
||||
const expectedText = `<section id="instructions">
|
||||
<p>These are the main instructions at depth 1.</p>
|
||||
<pre><code class="language-html"><div>Main instructions code</div>
|
||||
</code></pre>
|
||||
</section>`;
|
||||
|
||||
expect(file.data[instructionsId]).toEqual(expectedText);
|
||||
});
|
||||
|
||||
it('should have an output to match the snapshot', () => {
|
||||
const plugin = addText([descriptionId, instructionsId]);
|
||||
plugin(mockAST, file);
|
||||
|
||||
@@ -97,3 +97,4 @@ function idToData(node, index, parent, seeds) {
|
||||
}
|
||||
|
||||
module.exports.getFileVisitor = getFileVisitor;
|
||||
module.exports.getFilenames = getFilenames;
|
||||
|
||||
@@ -34,18 +34,19 @@ function _getSection(tree) {
|
||||
};
|
||||
}
|
||||
|
||||
const startNode = marker => ({
|
||||
const startNode = (marker, depth) => ({
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: marker
|
||||
}
|
||||
]
|
||||
],
|
||||
...((typeof depth === 'number' && { depth }) || {})
|
||||
});
|
||||
|
||||
function getSection(tree, marker) {
|
||||
const start = find(tree, startNode(marker));
|
||||
function getSection(tree, marker, depth) {
|
||||
const start = find(tree, startNode(marker, depth));
|
||||
return _getSection(tree)(start);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,20 +15,17 @@ describe('getSection', () => {
|
||||
});
|
||||
|
||||
it('should return an array', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getSection(simpleAst, '--hints--');
|
||||
expect(isArray(actual)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return an empty array if the marker is not present', () => {
|
||||
expect.assertions(2);
|
||||
const actual = getSection(simpleAst, '--not-a-marker--');
|
||||
expect(isArray(actual)).toBe(true);
|
||||
expect(actual.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should include any headings without markers', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getSection(extraHeadingAst, '--description--');
|
||||
expect(
|
||||
find(root(actual), {
|
||||
@@ -38,7 +35,6 @@ describe('getSection', () => {
|
||||
});
|
||||
|
||||
it('should include the rest of the AST if there is no end marker', () => {
|
||||
expect.assertions(2);
|
||||
const actual = getSection(extraHeadingAst, '--solutions--');
|
||||
expect(actual.length > 0).toBe(true);
|
||||
expect(
|
||||
@@ -46,6 +42,11 @@ describe('getSection', () => {
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should ignore a marker if the depth is not correct', () => {
|
||||
const actual = getSection(extraHeadingAst, '--instructions--', 2);
|
||||
expect(actual).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should match the hints snapshot', () => {
|
||||
const actual = getSection(simpleAst, '--hints--');
|
||||
expect(actual).toMatchSnapshot();
|
||||
|
||||
@@ -13,6 +13,7 @@ const VALID_MARKERS = [
|
||||
'# --fillInTheBlank--',
|
||||
'# --hints--',
|
||||
'# --instructions--',
|
||||
'# --interactive--',
|
||||
'# --notes--',
|
||||
'# --questions--',
|
||||
'# --quizzes--',
|
||||
|
||||
@@ -85,8 +85,8 @@ id: test
|
||||
title: Test
|
||||
---
|
||||
|
||||
## --instructions--
|
||||
Instructions should be at level 1, not 2.
|
||||
## --interactive--
|
||||
Interactive should be at level 1, not 2.
|
||||
|
||||
### --seed-contents--
|
||||
Seed contents should be at level 2, not 3.
|
||||
@@ -95,7 +95,7 @@ Seed contents should be at level 2, not 3.
|
||||
expect(() => {
|
||||
processor.runSync(processor.parse(file));
|
||||
}).toThrow(
|
||||
'Invalid heading levels: "## --instructions--" should be "# --instructions--", "### --seed-contents--" should be "## --seed-contents--".'
|
||||
'Invalid heading levels: "## --interactive--" should be "# --interactive--", "### --seed-contents--" should be "## --seed-contents--".'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -105,7 +105,7 @@ id: test
|
||||
title: Test
|
||||
---
|
||||
|
||||
## --instructions--
|
||||
## --interactive--
|
||||
Wrong level.
|
||||
|
||||
# --invalid-marker--
|
||||
@@ -118,7 +118,7 @@ Wrong level.
|
||||
expect(() => {
|
||||
processor.runSync(processor.parse(file));
|
||||
}).toThrow(
|
||||
'Invalid marker names: "--invalid-marker--".\nInvalid heading levels: "## --instructions--" should be "# --instructions--", "### --seed-contents--" should be "## --seed-contents--".'
|
||||
'Invalid marker names: "--invalid-marker--".\nInvalid heading levels: "## --interactive--" should be "# --interactive--", "### --seed-contents--" should be "## --seed-contents--".'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user