diff --git a/docs/docs/sdk/reveal.mdx b/docs/docs/sdk/reveal.mdx new file mode 100644 index 0000000000..7c73658064 --- /dev/null +++ b/docs/docs/sdk/reveal.mdx @@ -0,0 +1,165 @@ +# Reveal SDK + +import LiveCodes from '../../src/components/LiveCodes.tsx'; + +The Reveal.js SDK is a lightweight plugin that integrates the JavaScript SDK into Reveal.js, allowing you to embed interactive playgrounds directly within your slides. + +It has a very simple [implementation](https://github.com/live-codes/livecodes/blob/develop/src/sdk/reveal.ts) which you can easily modify in case you need. + +## Installation + +Please refer to the [SDK installation](./index.mdx#installation) section. + +## Usage + +To provide a mounting container, you just need to add the attribute data-livecodes to your container element, and then import the LiveCodes script. + +```html +
+``` + +There are two ways you can import the LiveCodes script + +1. Using a ` + + + +``` + +2. Using an ES module `import` statement +```js +import Reveal from "reveal.js"; +import { LiveCodes } from 'livecodes'; + +Reveal.initialize({ + plugins: [LiveCodes], +}); +``` + +### TypeScript Support + +TypeScript types are exported from this module. + +`LiveOptions` – Configuration options for an individual playground, including the optional sdkReady callback. + +`GlobalLiveCodesOptions` – Global configuration for LiveCodes when used with Reveal.js, including optional customStyle. + +`LiveCodesInstance` – The LiveCodes object itself, containing an id and an init method for initializing with a Reveal.js deck. + +This allows TypeScript projects to import these types directly for full type safety and autocompletion: + +```ts +import type { LiveOptions, GlobalLiveCodesOptions, LiveCodesInstance } from 'livecodes'; +const myPlaygroundOptions: LiveOptions = { + sdkReady: (sdk) => { + console.log("Playground initialized:", sdk); + }, + theme: "dark", +}; + +const deckOptions: GlobalLiveCodesOptions = { + livecodes: myPlaygroundOptions, + customStyle: { + border: "1px solid #ddd", + borderRadius: "8px", + }, + controls: true, + slideNumber: true, +}; +``` + +### Config + +All [embed options](js-ts.mdx#embed-options) are available as config object with the corresponding key-values. If you don’t specify it, the default configuration object will be used. + +Example: + +```js +import { LiveCodes } from 'livecodes'; + +const deck = new Reveal({ + plugins: [LiveCodes, Markdown], + livecodes: { + config:{ + markup: { + language: "markdown", + content: "# hello World!", + }, + } + sdkReady: (item) => { + console.log(item); + }, + } +}); + +deck.initialize(); +``` + +You can provide an optional `sdkReady?: (sdk: Playground) => void` property, which will be called as a callback function after the initialization is complete. A callback function, that is provided with an instance of the JavaScript SDK representing the current playground. This allows making use of full capability of the SDK by calling SDK methods. + +In addition, you can also pass a CSS object `customStyle` to specify the CSS properties of the editor. + +Example: + +```js +import { LiveCodes } from 'livecodes'; + +const deck = new Reveal({ + plugins: [LiveCodes, Markdown], + livecodes: { + config:{ + markup: { + language: "markdown", + content: "# hello World!", + }, + } + }, + customStyle: { border: "5px solid pink", borderRadius: "8px" }, +}); + +deck.initialize(); +``` + +The CSS properties in customStyle will be applied directly to the embedded iframe. For example, with the settings above, you will see rounded corners and a pink border on the iframe. + +:::info +The customStyle object changes the CSS properties of the embedded iframe itself, not the outer container’s CSS properties. +::: + +If you want to apply specific configurations to a particular LiveCode editor, you can write a stringified object that conforms to [JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#:~:text=The%20JSON.parse()%20static,object%20before%20it%20is%20returned.) into the data-config attribute of its container element. + +Example: +```html +
+ Slide 1 +
+
+``` + +## Demo + +export const sdkDemo = { + html:'\n\n
\n
\n
Slide 1\n
\n
\n
Slide 2\n
\n
\n
\n
\n\n\n' +}; + + + +## Related + +- [SDK Installation](./index.mdx#installation) +- [JS/TS SDK](./js-ts.mdx) +- [Vue SDK](./vue.mdx) +- [React SDK](./react.mdx) +- [Using SDK in Svelte](./svelte.mdx) +- [Using SDK in Solid](./solid.mdx) +- [Embedded Playgrounds](../features/embeds.mdx) diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 904912f997..7aad6ac021 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -85,7 +85,7 @@ const sidebars: SidebarsConfig = { type: 'doc', id: 'sdk/index', }, - items: ['sdk/js-ts', 'sdk/react', 'sdk/vue', 'sdk/svelte', 'sdk/solid', 'sdk/headless'], + items: ['sdk/js-ts', 'sdk/react', 'sdk/vue', 'sdk/svelte', 'sdk/solid', 'sdk/headless', "sdk/reveal"], }, { type: 'category', diff --git a/package-lock.json b/package-lock.json index 74f1dc6bee..db779165b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "monaco-editor": "0.48.0", "patch-package": "7.0.2", "prismjs": "1.29.0", + "reveal.js": "^5.2.1", "split.js": "1.6.5", "yjs": "13.5.40" }, @@ -47,6 +48,7 @@ "@types/prettier": "2.1.6", "@types/prismjs": "1.16.3", "@types/react": "19.0.10", + "@types/reveal.js": "^5.2.1", "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "8.24.1", "@typescript/vfs": "1.5.3", @@ -4148,6 +4150,12 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/reveal.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@types/reveal.js/-/reveal.js-5.2.1.tgz", + "integrity": "sha512-egr+amW5iilXo94kEGyJv24bJozsu/XAOHnhMHLnaJkHVxoui2gsWqzByaltA5zfXDTH2F4WyWnAkhHRcpytIQ==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -16811,6 +16819,14 @@ "node": ">=0.10.0" } }, + "node_modules/reveal.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/reveal.js/-/reveal.js-5.2.1.tgz", + "integrity": "sha512-r7//6mIM5p34hFiDMvYfXgyjXqGRta+/psd9YtytsgRlrpRzFv4RbH76TXd2qD+7ZPZEbpBDhdRhJaFgfQ7zNQ==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -22857,6 +22873,12 @@ "csstype": "^3.0.2" } }, + "@types/reveal.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@types/reveal.js/-/reveal.js-5.2.1.tgz", + "integrity": "sha512-egr+amW5iilXo94kEGyJv24bJozsu/XAOHnhMHLnaJkHVxoui2gsWqzByaltA5zfXDTH2F4WyWnAkhHRcpytIQ==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -32334,6 +32356,11 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "reveal.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/reveal.js/-/reveal.js-5.2.1.tgz", + "integrity": "sha512-r7//6mIM5p34hFiDMvYfXgyjXqGRta+/psd9YtytsgRlrpRzFv4RbH76TXd2qD+7ZPZEbpBDhdRhJaFgfQ7zNQ==" + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", diff --git a/package.json b/package.json index ee375d445b..1af8e90bf9 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "monaco-editor": "0.48.0", "patch-package": "7.0.2", "prismjs": "1.29.0", + "reveal.js": "^5.2.1", "split.js": "1.6.5", "yjs": "13.5.40" }, @@ -110,6 +111,7 @@ "@types/prettier": "2.1.6", "@types/prismjs": "1.16.3", "@types/react": "19.0.10", + "@types/reveal.js": "^5.2.1", "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "8.24.1", "@typescript/vfs": "1.5.3", @@ -158,15 +160,32 @@ "singleQuote": true, "trailingComma": "all", "printWidth": 100, - "plugins": ["prettier-plugin-organize-imports"] + "plugins": [ + "prettier-plugin-organize-imports" + ] }, "jest": { "preset": "ts-jest", "testEnvironment": "jsdom", - "setupFiles": ["/.jest/setup.ts"], - "testPathIgnorePatterns": ["/node_modules/", "/build/", "/src/modules/"], - "collectCoverageFrom": ["src/**/*.ts", "!**/build/**", "!**/vendor/**", "!src/modules/**"], - "coverageReporters": ["json", "html", "lcov"], + "setupFiles": [ + "/.jest/setup.ts" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/build/", + "/src/modules/" + ], + "collectCoverageFrom": [ + "src/**/*.ts", + "!**/build/**", + "!**/vendor/**", + "!src/modules/**" + ], + "coverageReporters": [ + "json", + "html", + "lcov" + ], "resolveJsonModule": true } } diff --git a/scripts/build.js b/scripts/build.js index 60b1976642..70907dcdc9 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -145,6 +145,19 @@ const sdkBuild = async () => { '@vue/runtime-core': 'vue', }, }), + esbuild.build({ + ...sdkOptions, + entryPoints: [sdkSrcDir + 'reveal.ts'], + outdir: undefined, + outfile: path.resolve(outDir, sdkOutDir, 'reveal.umd.js'), + format: 'iife', + }), + esbuild.build({ + ...sdkOptions, + entryPoints: [sdkSrcDir + 'reveal.ts'], + outdir: undefined, + outfile: path.resolve(outDir, sdkOutDir, 'reveal.js'), + }), ]); }; diff --git a/src/sdk/__tests__/reveal.test.ts b/src/sdk/__tests__/reveal.test.ts new file mode 100644 index 0000000000..ba7a85318e --- /dev/null +++ b/src/sdk/__tests__/reveal.test.ts @@ -0,0 +1,83 @@ +import { createPlayground } from '../index'; +import { LiveCodes } from '../reveal'; + +function expectIframeDefaultStyle(iframe: HTMLIFrameElement | null) { + expect(iframe?.style.maxWidth).toBe('100%'); + expect(iframe?.style.maxHeight).toBe('100%'); +} + +function expectCreatePlaygroundAndSdkFn(sdkReady: jest.MockedFunction<() => void>) { + expect(createPlayground).toHaveBeenCalledTimes(1); + expect(sdkReady).toHaveBeenCalledTimes(1); +} + +function createContainer(config: string = '') { + const container = document.createElement('div'); + container.dataset.livecodes = ''; + if (config.length > 0) container.dataset.config = config; + document.body.appendChild(container); +} + +function getIframe() { + return document.querySelector('.livecodes'); +} + +jest.mock('../index', () => ({ + createPlayground: jest.fn().mockImplementation((container) => { + const iframe = document.createElement('iframe'); + iframe.className = 'livecodes'; + container.appendChild(iframe); + return Promise.resolve({ playground: 'mocked' }); + }), +})); + +beforeEach(() => { + document.body.innerHTML = ''; + jest.clearAllMocks(); +}); + +test('should do nothing when no [data-livecodes] element exists', async () => { + const mockDeck = { getConfig: jest.fn().mockReturnValue({ livecodes: {} }) } as any; + LiveCodes.init(mockDeck); + expect(createPlayground).not.toHaveBeenCalled(); +}); + +test('should initializes playground and triggers sdkReady when a livecodes container with global config exists', async () => { + const sdkReady = jest.fn(); + createContainer(); + const mockDeck = { + getConfig: jest.fn().mockReturnValue({ + livecodes: { + config: { script: { language: 'javascript', content: 'console.log(456)' } }, + sdkReady, + }, + customStyle: { backgroundColor: 'rgba(255,255,255,0.1)' }, + }), + } as any; + LiveCodes.init(mockDeck); + await new Promise(process.nextTick); + expectCreatePlaygroundAndSdkFn(sdkReady); + const calledWith = (createPlayground as jest.Mock).mock.calls[0][1]; + const iframe = getIframe(); + expect(calledWith.config.script.language).toBe('javascript'); + expect(calledWith.config.script.content).toBe('console.log(456)'); + expectIframeDefaultStyle(iframe); + expect(iframe?.style.backgroundColor).toBe('rgba(255, 255, 255, 0.1)'); +}); + +test('should initialize multiple playgrounds when multiple livecodes containers exist', async () => { + const sdkReady = jest.fn(); + createContainer('{"config":{"script":{"language":"javascript","content":"console.log(1)"}}}'); + createContainer('{"config":{"script":{"language":"javascript","content":"console.log(2)"}}}'); + const mockDeck = { + getConfig: jest.fn().mockReturnValue({ + livecodes: { sdkReady }, + }), + } as any; + LiveCodes.init(mockDeck); + await new Promise(process.nextTick); + expect(createPlayground).toHaveBeenCalledTimes(2); + expect(sdkReady).toHaveBeenCalledTimes(2); + const iframes = document.querySelectorAll('.livecodes'); + expect(iframes.length).toBe(2); +}); diff --git a/src/sdk/package.sdk.json b/src/sdk/package.sdk.json index 54d5841863..4a5882acd1 100644 --- a/src/sdk/package.sdk.json +++ b/src/sdk/package.sdk.json @@ -14,6 +14,7 @@ "module": "./livecodes.js", "browser": "./livecodes.js", "types": "./livecodes.d.ts", + "jsdelivr": "./livecodes.js", "exports": { ".": { "import": "./livecodes.js", @@ -26,6 +27,9 @@ "./vue": { "import": "./vue.js" }, + "./reveal": { + "import": "./reveal.js" + }, "./package.json": "./package.json" } } diff --git a/src/sdk/reveal.ts b/src/sdk/reveal.ts new file mode 100644 index 0000000000..3aa3d3c92a --- /dev/null +++ b/src/sdk/reveal.ts @@ -0,0 +1,98 @@ +import type Reveal from 'reveal.js'; +import { createPlayground } from './index'; +// eslint-disable-next-line import/order +import type { Config, EmbedOptions, Playground } from './models'; + +export interface LiveOptions extends EmbedOptions { + sdkReady?: (sdk: Playground) => void; +} + +export interface GlobalLiveCodesOptions extends Reveal.Options { + livecodes?: LiveOptions; + customStyle?: Partial; +} + +export interface LiveCodesInstance { + id: string; + init(deck: InstanceType): void; +} + +const initIframeStyle = (iframe: HTMLIFrameElement, styles: Partial) => { + for (const [key, value] of Object.entries(styles)) { + // @ts-ignore + iframe.style[key as any] = value; + } +}; + +const applyConfigAndSdkFn = async function ( + sdkItem: Playground, + sdkReadyfn?: (sdk: Playground) => void, + config?: string | Partial, +) { + if (typeof config === 'string') { + await fetch(config) + .then((res) => res.json()) + .then((json) => sdkItem.setConfig(json)); + } + if (typeof sdkReadyfn === 'function') { + sdkReadyfn(sdkItem); + } +}; + +export const LiveCodes = { + id: 'LiveCodes', + init(deck: InstanceType) { + const ContainerList = document.querySelectorAll('[data-livecodes]'); + if (ContainerList.length < 1) { + return; + } + const containers = Array.from(ContainerList); + const config = deck.getConfig() as GlobalLiveCodesOptions; + const globalOptions = config.livecodes || {}; + const sdkReadyfn = config.livecodes?.sdkReady; + const customStyle = config.customStyle || {}; + const promises = containers.map((container) => { + const localOptions = container.dataset.config || '{}'; + const parsedLocalOptions = JSON.parse(localOptions); + let finalOptions: EmbedOptions; + if (typeof globalOptions.config === 'string') { + finalOptions = { + ...globalOptions, + ...parsedLocalOptions, + config: { + ...parsedLocalOptions.config, + }, + }; + } else { + finalOptions = { + ...globalOptions, + ...parsedLocalOptions, + config: { + ...globalOptions.config, + ...parsedLocalOptions.config, + }, + }; + } + if ( + typeof finalOptions.config === 'object' && + Object.keys(finalOptions.config).length === 0 + ) { + delete finalOptions.config; + } + return createPlayground(container, finalOptions); + }); + Promise.all(promises).then((sdk) => { + const iframes = document.querySelectorAll('.livecodes'); + iframes.forEach((iframe) => + initIframeStyle(iframe, { maxWidth: '100%', maxHeight: '100%', ...customStyle }), + ); + for (const sdkItem of sdk) { + applyConfigAndSdkFn(sdkItem, sdkReadyfn, globalOptions.config); + } + }); + }, +}; + +if (typeof window !== 'undefined') { + (window as any).LiveCodes = LiveCodes; +}