diff --git a/README.md b/README.md index 4d0cb49cc9..ebdf21c220 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A [feature-rich](https://livecodes.io/docs/features/), open-source, **client-sid [![LiveCodes: npm version](https://img.shields.io/npm/v/livecodes)](https://www.npmjs.com/package/livecodes) [![LiveCodes: npm downloads](https://img.shields.io/npm/dm/livecodes)](https://www.npmjs.com/package/livecodes) [![LiveCodes: jsdelivr downloads](https://data.jsdelivr.com/v1/package/npm/livecodes/badge?style=rounded)](https://www.jsdelivr.com/package/npm/livecodes) -[![LiveCodes: languages](https://img.shields.io/badge/languages-98-blue)](https://livecodes.io/docs/languages/) +[![LiveCodes: languages](https://img.shields.io/badge/languages-100-blue)](https://livecodes.io/docs/languages/) [![LiveCodes: docs](https://img.shields.io/badge/Documentation-575757?logo=gitbook&logoColor=white)](https://livecodes.io/docs/) [![LiveCodes: llms.txt](https://img.shields.io/badge/llms.txt-575757?logo=googledocs&logoColor=white)](https://livecodes.io/docs/llms.txt) [![LiveCodes: llms-full.txt](https://img.shields.io/badge/llms--full.txt-575757?logo=googledocs&logoColor=white)](https://livecodes.io/docs/llms-full.txt) diff --git a/docs/docs/configuration/configuration-object.mdx b/docs/docs/configuration/configuration-object.mdx index e8a946dba2..4e9dfefb6c 100644 --- a/docs/docs/configuration/configuration-object.mdx +++ b/docs/docs/configuration/configuration-object.mdx @@ -141,6 +141,12 @@ An object that configures the language and content of the markup editor. This ca A URL to load `content` from. It has to be a valid URL that is CORS-enabled. The URL is only fetched if `content` property had no value. +- `hidden`: + Type: [`boolean | undefined`](../api/interfaces/Config.md#hidden) + Default: `""` + If `true`, the title of the code editor is hidden, however its code is still evaluated. + This can be useful in embedded playgrounds (e.g. for hiding unnecessary code). + - `hiddenContent`: Type: [`string | undefined`](../api/interfaces/Config.md#hiddencontent) Default: `undefined` @@ -153,18 +159,12 @@ An object that configures the language and content of the markup editor. This ca A URL to load `hiddenContent` from. It has to be a valid URL that is CORS-enabled. The URL is only fetched if `hiddenContent` property had no value. -- `foldedLines`: - Type: [`Array<{ from: number; to: number }> | undefined`](../api/interfaces/Config.md#foldedlines) - Default: `undefined` - Lines that get folded when the editor loads. The code can be unfolded by clicking on arrow beside the line. - This can be useful for less relevant code in embedded playgrounds. - - `title`: Type: [`string | undefined`](../api/interfaces/Config.md#title) Default: `""` If set, this is used as the title of the editor in the UI, overriding the default title set to the language name (e.g. "Python" can be used instead of "Py (Wasm)"). -- `hideTitle`: +- `hideTitle` (**deprecated**, use `hidden` instead): Type: [`boolean | undefined`](../api/interfaces/Config.md#hidetitle) Default: `""` If `true`, the title of the code editor is hidden, however its code is still evaluated. @@ -186,6 +186,12 @@ An object that configures the language and content of the markup editor. This ca The initial position of the cursor in the code editor. Example: `{lineNumber: 5, column: 10}` +- `foldedLines`: + Type: [`Array<{ from: number; to: number }> | undefined`](../api/interfaces/Config.md#foldedlines) + Default: `undefined` + Lines that get folded when the editor loads. The code can be unfolded by clicking on arrow beside the line. + This can be useful for less relevant code in embedded playgrounds. + ### `style` Type: [`Editor`](../api/interfaces/Config.md#style) diff --git a/docs/docs/features/import.mdx b/docs/docs/features/import.mdx index adc81f6c4b..bfbb1cbff5 100644 --- a/docs/docs/features/import.mdx +++ b/docs/docs/features/import.mdx @@ -81,7 +81,7 @@ Import is supported from any of the following: - Local file(s) - Code in zip file (Local or URL) - Code in image - OCR (Local or URL) -- Projects shared in official playgrounds of [TypeScript](https://www.typescriptlang.org/play) and [Vue](https://play.vuejs.org/) +- Projects shared in official playgrounds of [TypeScript](https://www.typescriptlang.org/play), [Vue](https://play.vuejs.org/), [Svelte](https://svelte.dev/playground), [Preact](https://preactjs.com/repl/) and [Solid](https://playground.solidjs.com/) - [Exported project JSON](./export.mdx) (single project and bulk import) Import sources are identified by URL patterns (e.g. origin, pathname and extension). @@ -90,34 +90,6 @@ Import sources are identified by URL patterns (e.g. origin, pathname and extensi Local files can be imported from the "Import Screen" or by dragging and dropping the file(s) in the editor. ::: -## File Selection - -For sources that provide multiple files (e.g. GitHub/GitLab directories, GitHub gists, GitLab snippets and local files), a best guess is tried to load files in respective editors. Best results are when there are 3 files and each file is in a language (identified by file extension) that can be loaded to a different editor, for example: - -- index.html, style.css, script.js -- default.pug, app.scss, main.ts - -The following file names are given higher priority: - -- Markup files starting with `index.` or `default.` -- Style files starting with `style.` or `styles.` -- Script files starting with `script.`, `app.`, `main.` or `index.` - -While README, markdown files and files with no extension are given lower priority. - -Alternatively, files can be specified using the `files` [query param](../configuration/query-params.mdx). It takes a **comma-separated list** of filenames. The first 3 found files are loaded. If 1 or 2 files are specified, only these will be loaded. The first matching file is shown by default in the active editor. - -The query params should have the following format: -`?x={url}&files={file1},{file2},{file3}` - -Example: -`?x={url}&files=Counter.tsx,counter.scss,counter.html` - -The active editor can be specified using the [`activeEditor`](../configuration/configuration-object.mdx#activeeditor) (or its alias `active`) [query param](../configuration/query-params.mdx). It takes the name of the editor (`markup`, `style` or `script`) or its ID (`0`, `1` or `2`) to be shown by default. - -Example: -`?x={url}&activeEditor=style` or `?x={url}&active=1` - ## Import Shared Projects [Shared Projects](./share.mdx) can be imported using the value of the query param `x` generated by the Share screen. This starts with either: diff --git a/docs/docs/languages/cssmodules.mdx b/docs/docs/languages/cssmodules.mdx index 11579dfd82..95df85639c 100644 --- a/docs/docs/languages/cssmodules.mdx +++ b/docs/docs/languages/cssmodules.mdx @@ -8,7 +8,7 @@ The selector names are unique to avoid naming collision. They can then be import CSS Modules can be enabled from the style editor menu. -Selectors added to the style editor (using any language e.g. CSS, SCSS, Less, etc.) are transformed to unique selectors. The transformed classes are then accessible in the script editor as a JSON object, and are injected into the HTML elements. +Selectors added to the style editor (using any language e.g. CSS, SCSS, Less, etc.) are transformed to unique selectors. The transformed classes are then accessible in the script editor as a JSON object, and can be injected into the HTML elements. **Example:** @@ -193,7 +193,7 @@ In addition, the following settings are available: - `addClassesToHTML` - Type: `boolean`. Default: `true`. + Type: `boolean`. Default: `false`. The generated classes are injected into the HTML elements, so the styles are applied without having to assign them using JavaScript. @@ -212,7 +212,7 @@ Please note that custom settings should be valid JSON (i.e. functions are not al "cssmodules": { "exportGlobals": true, "localsConvention": "camelCaseOnly", - "addClassesToHTML": false + "addClassesToHTML": true } } ``` @@ -234,16 +234,30 @@ If you get this working, [please create a pull request](https://github.com/live- import LiveCodes from '../../src/components/LiveCodes.tsx'; -export const params = { +export const config = { activeEditor: 'style', - html: '
\n

Page Title

\n

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Dolore earum blanditiis quidem non beatae ipsam autem maiores ut et delectus unde repudiandae, repellendus aut. Aspernatur similique facere facilis minima tempora.

\n
\n', - css: ':global .page {\n padding: 20px;\n}\n\n.text {\n color: black;\n font-family: sans-serif;\n}\n\n.small-text {\n composes: text;\n font-size: 20px;\n}\n\n.large-text {\n composes: text;\n font-size: 40px;\n}\n\n.large-text:hover {\n color: red;\n}\n\n.title {\n composes: large-text;\n color: green;\n}\n', - js: "import classes from './style.module.css';\n\ndocument.querySelector('h1').className = classes.title;\nconsole.log(classes);\n", - processors: 'cssmodules', - compiled: 'open', -}; - - + markup: { + language: 'html', + content: '
\n

Page Title

\n

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Dolore earum blanditiis quidem non beatae ipsam autem maiores ut et delectus unde repudiandae, repellendus aut. Aspernatur similique facere facilis minima tempora.

\n
\n', + }, + style: { + language: 'css', + content: ':global .page {\n padding: 20px;\n}\n\n.text {\n color: black;\n font-family: sans-serif;\n}\n\n.small-text {\n composes: text;\n font-size: 20px;\n}\n\n.large-text {\n composes: text;\n font-size: 40px;\n}\n\n.large-text:hover {\n color: red;\n}\n\n.title {\n composes: large-text;\n color: green;\n}\n', + }, + script: { + language: 'js', + content: "import classes from './style.module.css';\n\ndocument.querySelector('h1').className = classes.title;\nconsole.log(classes);\n", + }, + processors: ['cssmodules'], + tools: { active: 'compiled' }, + customSettings: { + cssmodules: { + addClassesToHTML: true, + }, + } +} + + ## Links diff --git a/docs/docs/languages/html.mdx b/docs/docs/languages/html.mdx index 88498534b8..7d44329f3a 100644 --- a/docs/docs/languages/html.mdx +++ b/docs/docs/languages/html.mdx @@ -48,7 +48,7 @@ or adding JavaScript together with a different script editor language (e.g. see ### Extensions -`.html`, `.htm` +`.html`, `.htm`, `.svg` ### Editor diff --git a/docs/docs/languages/json.mdx b/docs/docs/languages/json.mdx new file mode 100644 index 0000000000..3c7dbc17c0 --- /dev/null +++ b/docs/docs/languages/json.mdx @@ -0,0 +1,3 @@ +# JSON + +TODO ... diff --git a/docs/docs/languages/json5.mdx b/docs/docs/languages/json5.mdx new file mode 100644 index 0000000000..27bf778a0c --- /dev/null +++ b/docs/docs/languages/json5.mdx @@ -0,0 +1,3 @@ +# JSON5 + +TODO ... diff --git a/docs/docs/languages/jsonc.mdx b/docs/docs/languages/jsonc.mdx new file mode 100644 index 0000000000..030ad2e69f --- /dev/null +++ b/docs/docs/languages/jsonc.mdx @@ -0,0 +1,3 @@ +# JSONC + +TODO ... diff --git a/docs/docs/languages/svelte.mdx b/docs/docs/languages/svelte.mdx index ce4c89aef0..a78f31998b 100644 --- a/docs/docs/languages/svelte.mdx +++ b/docs/docs/languages/svelte.mdx @@ -116,7 +116,7 @@ import Counter from './Counter.svelte'; -Please note that LiveCodes [does not have the concept of a file system](../features/projects.mdx). However, you can configure [editor options](../configuration/configuration-object.mdx#markup) like `title`, `order` and `hideTitle` to simulate multiple files, change editor order or even hide editors. +Please note that LiveCodes [does not have the concept of a file system](../features/projects.mdx). However, you can configure [editor options](../configuration/configuration-object.mdx#markup) like `title`, `order` and `hidden` to simulate multiple files, change editor order or even hide editors. Example: diff --git a/docs/docs/languages/text.mdx b/docs/docs/languages/text.mdx new file mode 100644 index 0000000000..d80b25de3c --- /dev/null +++ b/docs/docs/languages/text.mdx @@ -0,0 +1,3 @@ +# Text + +TODO ... diff --git a/docs/docs/languages/vue.mdx b/docs/docs/languages/vue.mdx index ff0928adad..01abb418d0 100644 --- a/docs/docs/languages/vue.mdx +++ b/docs/docs/languages/vue.mdx @@ -188,7 +188,7 @@ import Counter from './Counter.vue'; -Please note that LiveCodes [does not have the concept of a file system](../features/projects.mdx). However, you can configure [editor options](../configuration/configuration-object.mdx#markup) like `title`, `order` and `hideTitle` to simulate multiple files, change editor order or even hide editors. +Please note that LiveCodes [does not have the concept of a file system](../features/projects.mdx). However, you can configure [editor options](../configuration/configuration-object.mdx#markup) like `title`, `order` and `hidden` to simulate multiple files, change editor order or even hide editors. Example: diff --git a/docs/docs/languages/yaml.mdx b/docs/docs/languages/yaml.mdx new file mode 100644 index 0000000000..85a014e9c7 --- /dev/null +++ b/docs/docs/languages/yaml.mdx @@ -0,0 +1,3 @@ +# YAML + +TODO ... diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index e1a4c6dbfc..eb0026647f 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -20,7 +20,7 @@ const config: Config = { tagline: 'A Code Playground That Just Works!', url: 'https://livecodes.io/', baseUrl, - onBrokenLinks: 'throw', + onBrokenLinks: 'warn', onBrokenMarkdownLinks: 'warn', favicon: 'img/favicon.ico', organizationName: 'LiveCodes', diff --git a/docs/package-lock.json b/docs/package-lock.json index b458c03814..7a583cc0e8 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -28,10 +28,10 @@ "@types/node": "22.13.0", "@types/react": "19.0.10", "docusaurus-plugin-typedoc": "1.0.5", - "typedoc": "0.26.11", - "typedoc-plugin-markdown": "4.2.10", - "typedoc-plugin-missing-exports": "3.1.0", - "typescript": "5.5.4" + "typedoc": "0.28.16", + "typedoc-plugin-markdown": "4.10.0", + "typedoc-plugin-missing-exports": "4.1.2", + "typescript": "5.9.3" } }, "node_modules/@algolia/autocomplete-core": { @@ -3818,6 +3818,20 @@ "node": ">=18.0" } }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.22.0.tgz", + "integrity": "sha512-jMpciqEVUBKE1QwU64S4saNMzpsSza6diNCk4MWAeCxO2+LFi2FIFmL2S0VDLzEJCxuvCbU783xi8Hp/gkM5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.22.0", + "@shikijs/langs": "^3.22.0", + "@shikijs/themes": "^3.22.0", + "@shikijs/types": "^3.22.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -4080,169 +4094,6 @@ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==" }, - "node_modules/@rspack/binding": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.2.5.tgz", - "integrity": "sha512-q9vQmGDFZyFVMULwOFL7488WNSgn4ue94R/njDLMMIPF4K0oEJP2QT02elfG4KVGv2CbP63D7vEFN4ZNreo/Rw==", - "optional": true, - "peer": true, - "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.2.5", - "@rspack/binding-darwin-x64": "1.2.5", - "@rspack/binding-linux-arm64-gnu": "1.2.5", - "@rspack/binding-linux-arm64-musl": "1.2.5", - "@rspack/binding-linux-x64-gnu": "1.2.5", - "@rspack/binding-linux-x64-musl": "1.2.5", - "@rspack/binding-win32-arm64-msvc": "1.2.5", - "@rspack/binding-win32-ia32-msvc": "1.2.5", - "@rspack/binding-win32-x64-msvc": "1.2.5" - } - }, - "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", - "integrity": "sha512-ou0NXMLp6RxY9Bx8P9lA8ArVjz/WAI/gSu5kKrdKKtMs6WKutl4vvP9A4HHZnISd9Tn00dlvDwNeNSUR7fjoDQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@rspack/binding-darwin-x64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", - "integrity": "sha512-RdvH9YongQlDE9+T2Xh5D2+dyiLHx2Gz38Af1uObyBRNWjF1qbuR51hOas0f2NFUdyA03j1+HWZCbE7yZrmI3w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", - "integrity": "sha512-jznk/CI/wN93fr8I1j3la/CAiGf8aG7ZHIpRBtT4CkNze0c5BcF3AaJVSBHVNQqgSv0qddxMt3SADpzV8rWZ6g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", - "integrity": "sha512-oYzcaJ0xjb1fWbbtPmjjPXeehExEgwJ8fEGYQ5TikB+p9oCLkAghnNjsz9evUhgjByxi+NTZ1YmUNwxRuQDY1Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", - "integrity": "sha512-dzEKs8oi86Vi+TFRCPpgmfF5ANL0VmlZN45e1An7HipeI2C5B1xrz/H8V43vPy8XEvQuMmkXO6Sp82A0zlHvIA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-linux-x64-musl": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", - "integrity": "sha512-4ENeVPVSD97rRRGr6kJSm4sIPf1tKJ8vlr9hJi4sSvF7eMLWipSwIVmqRXJ2riVMRjYD2einmJ9KzI8rqQ2OwA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", - "integrity": "sha512-WUoJvX/z43MWeW1JKAQIxdvqH02oLzbaGMCzIikvniZnakQovYLPH6tCYh7qD3p7uQsm+IafFddhFxTtogC3pg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.2.5.tgz", - "integrity": "sha512-YzPvmt/gpiacE6aAacz4dxgEbNWwoKYPaT4WYy/oITobnAui++iCFXC4IICSmlpoA1y7O8K3Qb9jbaB/lLhbwA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", - "integrity": "sha512-QDDshfteMZiglllm7WUh/ITemFNuexwn1Yul7cHBFGQu6HqtqKNAR0kGR8J3e15MPMlinSaygVpfRE4A0KPmjQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rspack/core": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.2.5.tgz", - "integrity": "sha512-x/riOl05gOVGgGQFimBqS5i8XbUpBxPIKUC+tDX4hmNNkzxRaGpspZfNtcL+1HBMyYuoM6fOWGyCp2R290Uy6g==", - "optional": true, - "peer": true, - "dependencies": { - "@module-federation/runtime-tools": "0.8.4", - "@rspack/binding": "1.2.5", - "@rspack/lite-tapable": "1.0.1", - "caniuse-lite": "^1.0.30001616" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@rspack/tracing": "^1.x", - "@swc/helpers": ">=0.5.1" - }, - "peerDependenciesMeta": { - "@rspack/tracing": { - "optional": true - }, - "@swc/helpers": { - "optional": true - } - } - }, "node_modules/@rspack/lite-tapable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz", @@ -4251,66 +4102,45 @@ "node": ">=16.0.0" } }, - "node_modules/@shikijs/core": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", - "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", - "dev": true, - "dependencies": { - "@shikijs/engine-javascript": "1.29.2", - "@shikijs/engine-oniguruma": "1.29.2", - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.4" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", - "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", - "dev": true, - "dependencies": { - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", - "oniguruma-to-es": "^2.2.0" - } - }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", - "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.22.0.tgz", + "integrity": "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==", "dev": true, + "license": "MIT", "dependencies": { - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1" + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", - "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.22.0.tgz", + "integrity": "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==", "dev": true, + "license": "MIT", "dependencies": { - "@shikijs/types": "1.29.2" + "@shikijs/types": "3.22.0" } }, "node_modules/@shikijs/themes": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", - "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.22.0.tgz", + "integrity": "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==", "dev": true, + "license": "MIT", "dependencies": { - "@shikijs/types": "1.29.2" + "@shikijs/types": "3.22.0" } }, "node_modules/@shikijs/types": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", - "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.22.0.tgz", + "integrity": "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==", "dev": true, + "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.1", + "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, @@ -4318,7 +4148,8 @@ "version": "10.0.2", "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@sideway/address": { "version": "4.1.5", @@ -7638,12 +7469,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, - "node_modules/emoji-regex-xs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", - "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", - "dev": true - }, "node_modules/emojilib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", @@ -9032,29 +8857,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "dev": true, - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.3.tgz", @@ -13015,17 +12817,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/oniguruma-to-es": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", - "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", - "dev": true, - "dependencies": { - "emoji-regex-xs": "^1.0.0", - "regex": "^5.1.1", - "regex-recursion": "^5.1.1" - } - }, "node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -15412,31 +15203,6 @@ "@babel/runtime": "^7.8.4" } }, - "node_modules/regex": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", - "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", - "dev": true, - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-recursion": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", - "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", - "dev": true, - "dependencies": { - "regex": "^5.1.1", - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-utilities": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", - "dev": true - }, "node_modules/regexpu-core": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", @@ -16282,22 +16048,6 @@ "node": ">=4" } }, - "node_modules/shiki": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", - "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", - "dev": true, - "dependencies": { - "@shikijs/core": "1.29.2", - "@shikijs/engine-javascript": "1.29.2", - "@shikijs/engine-oniguruma": "1.29.2", - "@shikijs/langs": "1.29.2", - "@shikijs/themes": "1.29.2", - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", - "@types/hast": "^3.0.4" - } - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -16975,46 +16725,50 @@ } }, "node_modules/typedoc": { - "version": "0.26.11", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.11.tgz", - "integrity": "sha512-sFEgRRtrcDl2FxVP58Ze++ZK2UQAEvtvvH8rRlig1Ja3o7dDaMHmaBfvJmdGnNEFaLTpQsN8dpvZaTqJSu/Ugw==", + "version": "0.28.16", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.16.tgz", + "integrity": "sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog==", "dev": true, + "license": "Apache-2.0", "dependencies": { + "@gerrit0/mini-shiki": "^3.17.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "shiki": "^1.16.2", - "yaml": "^2.5.1" + "yaml": "^2.8.1" }, "bin": { "typedoc": "bin/typedoc" }, "engines": { - "node": ">= 18" + "node": ">= 18", + "pnpm": ">= 10" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" } }, "node_modules/typedoc-plugin-markdown": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.2.10.tgz", - "integrity": "sha512-PLX3pc1/7z13UJm4TDE9vo9jWGcClFUErXXtd5LdnoLjV6mynPpqZLU992DwMGFSRqJFZeKbVyqlNNeNHnk2tQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.10.0.tgz", + "integrity": "sha512-psrg8Rtnv4HPWCsoxId+MzEN8TVK5jeKCnTbnGAbTBqcDapR9hM41bJT/9eAyKn9C2MDG9Qjh3MkltAYuLDoXg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 18" }, "peerDependencies": { - "typedoc": "0.26.x" + "typedoc": "0.28.x" } }, "node_modules/typedoc-plugin-missing-exports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-3.1.0.tgz", - "integrity": "sha512-Sogbaj+qDa21NjB3SlIw4JXSwmcl/WOjwiPNaVEcPhpNG/MiRTtpwV81cT7h1cbu9StpONFPbddYWR0KV/fTWA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-4.1.2.tgz", + "integrity": "sha512-WNoeWX9+8X3E3riuYPduilUTFefl1K+Z+5bmYqNeH5qcWjtnTRMbRzGdEQ4XXn1WEO4WCIlU0vf46Ca2y/mspg==", "dev": true, + "license": "MIT", "peerDependencies": { - "typedoc": "0.26.x || 0.27.x" + "typedoc": "^0.28.1" } }, "node_modules/typedoc/node_modules/brace-expansion": { @@ -17042,9 +16796,10 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18037,15 +17792,19 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yocto-queue": { diff --git a/docs/package.json b/docs/package.json index 438bd7ddd4..e708546b53 100644 --- a/docs/package.json +++ b/docs/package.json @@ -35,10 +35,10 @@ "@types/node": "22.13.0", "@types/react": "19.0.10", "docusaurus-plugin-typedoc": "1.0.5", - "typedoc": "0.26.11", - "typedoc-plugin-markdown": "4.2.10", - "typedoc-plugin-missing-exports": "3.1.0", - "typescript": "5.5.4" + "typedoc": "0.28.16", + "typedoc-plugin-markdown": "4.10.0", + "typedoc-plugin-missing-exports": "4.1.2", + "typescript": "5.9.3" }, "browserslist": { "production": [ diff --git a/docs/src/components/HomepageFeatures.tsx b/docs/src/components/HomepageFeatures.tsx index 53f03939f1..01e89389df 100644 --- a/docs/src/components/HomepageFeatures.tsx +++ b/docs/src/components/HomepageFeatures.tsx @@ -3,7 +3,7 @@ import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import CodeBlock from '@theme/CodeBlock'; -import clsx from 'clsx'; +import { clsx } from 'clsx'; import type { ReactNode } from 'react'; import HomepageCarousel from './HomepageCarousel'; import styles from './HomepageFeatures.module.css'; diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index dd21d4c655..a9259a0e6e 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -2,7 +2,7 @@ import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; -import clsx from 'clsx'; +import { clsx } from 'clsx'; import { useEffect } from 'react'; import HomepageFeatures from '../components/HomepageFeatures'; import { loadAds } from '../custom-content'; diff --git a/docs/src/theme/DocItem/Layout/index.tsx b/docs/src/theme/DocItem/Layout/index.tsx index 075fb94cad..6a26fed707 100644 --- a/docs/src/theme/DocItem/Layout/index.tsx +++ b/docs/src/theme/DocItem/Layout/index.tsx @@ -12,7 +12,7 @@ import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop'; import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile'; import DocVersionBadge from '@theme/DocVersionBadge'; import DocVersionBanner from '@theme/DocVersionBanner'; -import clsx from 'clsx'; +import { clsx } from 'clsx'; import { type ReactNode } from 'react'; import styles from './styles.module.css'; diff --git a/eslint.config.mjs b/eslint.config.mjs index aab8c872fb..9d283255e7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,8 +29,10 @@ export default [ '**/coverage', '**/scripts', '**/.docusaurus', + '**/.cache', '**/.jest', '**/.storybook', + 'docs/docs/api', 'functions/vendors', ], }, diff --git a/functions/utils.ts b/functions/utils.ts index e2772f8fb4..e0c81d0860 100644 --- a/functions/utils.ts +++ b/functions/utils.ts @@ -72,7 +72,8 @@ export const getProjectInfo = async (url: URL): Promise => { if (templateName) { return { title: templateName, - description: templateName + ' Template on LiveCodes', + description: + templateName + (templateName.includes('Template') ? '' : ' Template') + ' on LiveCodes', }; } return { diff --git a/functions/vendors/templates.js b/functions/vendors/templates.js index bdd0caf05f..aa97a86b4c 100644 --- a/functions/vendors/templates.js +++ b/functions/vendors/templates.js @@ -1,5 +1,1286 @@ var getTemplateName = (_, templateName) => templateName; -var d={name:"angular",title:getTemplateName("templates.starter.angular","Angular Starter"),thumbnail:"assets/templates/angular.svg",activeEditor:"script",markup:{language:"html",content:`Loading... +var d={name:"multifile-basic",title:getTemplateName("templates.multifile.basic","Basic Template"),thumbnail:"assets/templates/blank.svg",mainFile:"index.html",activeEditor:"index.html",files:[{filename:"index.html",language:"html",content:` + + + + + + + + + + `, forceCompile: forceCompileStyles, @@ -1014,15 +1405,12 @@ const getResultPage = async ({ runTests ? testsNotChanged ? Promise.resolve(getCache().tests?.compiled || '') - : compiler.compile(testsContent, testsLanguage, config, {}) + : compiler.compile(testsContent, testsLanguage, config, { filename: 'tests' }) : Promise.resolve(getCompileResult(getCache().tests?.compiled || '')), ]); const [compiledStyle, compiledTests] = [styleCompileResult, testsCompileResult].map((result) => { const { code, info } = getCompileResult(result); - compileInfo = { - ...compileInfo, - ...info, - }; + compileInfo = mergeCompileInfo(compileInfo, info); return code; }); @@ -1054,11 +1442,13 @@ const getResultPage = async ({ ...contentConfig.tests, compiled: compiledTests, }, + files: [], + mainFile: undefined, }; compiledCode.script.modified = compiledCode.script.compiled; if (scriptType != null && scriptType !== 'module') { - singleFile = true; + singleFileResult = true; } const result = await createResultPage({ @@ -1067,7 +1457,7 @@ const getResultPage = async ({ forExport, template, baseUrl, - singleFile, + singleFileResult, runTests, compileInfo, }); @@ -1082,7 +1472,7 @@ const getResultPage = async ({ logError(scriptLanguage, scriptCompileResult.info?.errors); logError(testsLanguage, getCompileResult(testsCompileResult).info?.errors); - if (singleFile) { + if (singleFileResult) { setCache({ ...getCache(), ...compiledCode, @@ -1101,12 +1491,161 @@ const getResultPage = async ({ return result; }; +const getMultiFileResultPage = async ({ + sourceEditor = undefined as EditorId | undefined, + forExport = false, + template = resultTemplate, + singleFileResult = true, + runTests = false, +}) => { + autoEnableProcessors(); + const config = getConfig(); + const cache = getCache(); + + const forceCompileStyles = [...config.processors, ...cache.processors].some((name) => + processors.find((p) => name === p.name && p.needsHTML), + ); + + const testsNotChanged = + (!config.tests?.content && !cache.tests?.content) || + (config.tests?.language === cache.tests?.language && + config.tests?.content === cache.tests?.content && + cache.tests?.compiled); + + if (testsNotChanged && !config.tests?.content) { + toolsPane?.tests?.showResults({ results: [] }); + } + + const compiledFiles: Array = []; + let compileInfo: CompileInfo = {}; + const errors: Array<{ language: Language; filename: string; errors: string[] }> = []; + + for (const file of config.files) { + const { filename, language, content } = file; + if (getLanguageEditorId(language) === 'style') continue; + const compileResult = await compiler.compile(content, language, config, { + filename, + compileInfo, + }); + compiledFiles.push({ ...file, compiled: compileResult.code }); + compileInfo = mergeCompileInfo(compileInfo, compileResult.info); + if (compileInfo.errors?.length) { + errors.push({ language, filename, errors: compileInfo.errors || [] }); + } + } + + const mainFile = compiledFiles.find((f) => f.filename === getMainFile(config)); + const compiledContent = + compiledFiles + .map((file) => + getLanguageEditorId(file.language) === 'markup' + ? file.compiled + : ``, + ) + .join('\n') + ``; + + for (const file of config.files) { + const { filename, language, content } = file; + if (getLanguageEditorId(language) !== 'style') continue; + const compileResult = await compiler.compile(content, language, config, { + filename, + compileInfo: { + ...compileInfo, + modifiedHTML: mainFile?.compiled || '', + }, + forceCompile: forceCompileStyles, + html: compiledContent, + }); + if (mainFile && compileResult.info.modifiedHTML) { + mainFile.compiled = compileResult.info.modifiedHTML; + } + compiledFiles.push({ ...file, compiled: compileResult.code }); + compileInfo = mergeCompileInfo(compileInfo, compileResult.info); + if (compileInfo.errors?.length) { + errors.push({ language, filename, errors: compileInfo.errors || [] }); + } + } + + const testsCompileResult = await (runTests + ? testsNotChanged + ? Promise.resolve(getCache().tests?.compiled || '') + : compiler.compile( + config.tests?.content || '', + config.tests?.language || 'javascript', + config, + { filename: 'tests' }, + ) + : Promise.resolve(getCompileResult(getCache().tests?.compiled || ''))); + const { code: compiledTests, info: testsCompileInfo } = getCompileResult(testsCompileResult); + if (testsCompileInfo?.errors?.length) { + errors.push({ + language: config.tests?.language || 'javascript', + filename: 'tests', + errors: testsCompileInfo.errors || [], + }); + } + + const result = await createMultiFileResultPage({ + compiledFiles, + compiledTests, + config, + forExport, + template, + baseUrl, + singleFileResult, + runTests, + compileInfo, + }); + + const styleOnlyUpdate = sourceEditor === 'style' && !compileInfo.cssModules; + + const logError = (language: Language, errors: string[] = []) => { + errors.forEach((err) => toolsPane?.console?.error(`[${getLanguageTitle(language)}] ${err}`)); + }; + errors.forEach(({ language, errors }) => logError(language, errors)); + + if (singleFileResult) { + setCache({ + ...getCache(), + files: compiledFiles, + mainFile: config.mainFile, + result: cleanResultFromDev(result), + styleOnlyUpdate, + }); + + if (broadcastInfo.isBroadcasting) { + broadcast(); + } + if (resultPopup && !resultPopup.closed) { + resultPopup?.postMessage({ result }, location.origin); + } + } + + return result; +}; + +const mergeCompileInfo = (compileInfo: CompileInfo, newCompileInfo: CompileInfo) => ({ + ...compileInfo, + ...newCompileInfo, + cssModules: { + ...compileInfo.cssModules, + ...newCompileInfo.cssModules, + }, + importedContent: (compileInfo.importedContent || '') + (newCompileInfo.importedContent || ''), + imports: { + ...compileInfo.imports, + ...newCompileInfo.imports, + }, +}); + const reloadCompiler = async (config: Config, force = false) => { if (!compiler.isFake && !force) return; compiler = (window as any).compiler = await getCompiler({ config, baseUrl, eventsManager, + getTypes: async (code: string) => + typeLoader.load(code, { ...config.types, ...config.customSettings.types }, true), }); setCache(); await getResultPage({}); @@ -1153,6 +1692,8 @@ const flushResult = () => { content: '', compiled: '', }, + files: [], + mainFile: undefined, }); updateCompiledCode(); @@ -1276,11 +1817,11 @@ const format = async (allEditors = true) => { ); } else { const activeEditor = getActiveEditor(); - await activeEditor.format(); + await activeEditor?.format(); if (getConfig().foldRegions) { - await activeEditor.foldRegions?.(); + await activeEditor?.foldRegions?.(); } - activeEditor.focus(); + activeEditor?.focus(); } updateConfig(); }; @@ -1327,31 +1868,34 @@ const share = async ( permanentUrl = false, ): Promise => { const config = getConfig(); - const content = contentOnly - ? { - ...getContentConfig(config), - markup: { - ...config.markup, - title: undefined, - hideTitle: undefined, - }, - style: { - ...config.style, - title: undefined, - hideTitle: undefined, - }, - script: { - ...config.script, - title: undefined, - hideTitle: undefined, - }, - tools: { - ...config.tools, - enabled: defaultConfig.tools.enabled, - status: config.tools.status === 'none' ? defaultConfig.tools.status : config.tools.status, - }, - } - : config; + const content = getSDKConfig( + contentOnly + ? { + ...getContentConfig(config), + markup: { + ...config.markup, + title: undefined, + hidden: undefined, + }, + style: { + ...config.style, + title: undefined, + hidden: undefined, + }, + script: { + ...config.script, + title: undefined, + hidden: undefined, + }, + tools: { + ...config.tools, + enabled: defaultConfig.tools.enabled, + status: + config.tools.status === 'none' ? defaultConfig.tools.status : config.tools.status, + }, + } + : config, + ); const currentUrl = (location.origin + location.pathname).split('/').slice(0, -1).join('/') + '/'; const appUrl = permanentUrl ? permanentUrlService.getAppUrl() : currentUrl; @@ -1381,24 +1925,38 @@ const share = async ( }; const updateConfig = () => { + const newConfig = getConfig(); editorIds.forEach((editorId) => { - setConfig({ - ...getConfig(), - [editorId]: { - ...getConfig()[editorId], - language: getEditorLanguage(editorId), + if ( + (editorId === 'markup' || editorId === 'style' || editorId === 'script') && + editors[editorId] + ) { + newConfig[editorId] = { + ...newConfig[editorId], + language: getEditorLanguage(editorId) as Language, content: editors[editorId].getValue(), - }, - }); + }; + } }); + newConfig.files = newConfig.files.map((file) => + editors[file.filename] + ? { + ...file, + language: getFileLanguage(file.filename, newConfig) as Language, + content: editors[file.filename].getValue(), + } + : file, + ); + setConfig(newConfig); }; const loadConfig = async ( - newConfig: Partial, + newConfig: Partial, url?: string, flush = true, ) => { changingContent = true; + const currentConfig = getConfig(); const validConfig = upgradeAndValidate(newConfig); const content = getContentConfig({ ...defaultConfig, @@ -1432,7 +1990,7 @@ const loadConfig = async ( iframeScrollPosition.x = 0; iframeScrollPosition.y = 0; - await applyConfig(config, /* reload= */ true); + await applyConfig(config, /* reload= */ true, currentConfig); changingContent = false; }; @@ -1440,12 +1998,15 @@ const loadConfig = async ( const applyConfig = async (newConfig: Partial, reload = false, oldConfig?: Config) => { const currentConfig = oldConfig || getConfig(); const combinedConfig: Config = { ...currentConfig, ...newConfig }; + for (const file of oldConfig?.files || []) { + deleteFile(file.filename); + } + configureMultiFile(combinedConfig); if (reload) { await updateEditors(editors, getConfig()); } phpHelper({ editor: editors.script }); setLoading(true); - await setActiveEditor(combinedConfig); if (!isEmbed) { loadSettings(combinedConfig); @@ -1469,9 +2030,6 @@ const applyConfig = async (newConfig: Partial, reload = false, oldConfig setConfig(combinedConfig); - if (!isEmbed) { - setTimeout(() => getActiveEditor().focus()); - } setExternalResourcesMark(); setProjectInfoMark(); setCustomSettingsMark(); @@ -1530,22 +2088,28 @@ const applyConfig = async (newConfig: Partial, reload = false, oldConfig const hasEditorConfig = Object.keys(editorConfig).some((k) => k in newConfig); let shouldReloadEditors = (() => { - if (newConfig.editor != null && !(newConfig.editor in editors.markup)) return true; + if (oldConfig?.files?.length || newConfig.files?.length) return true; + const activeEditor = getActiveEditor(); + if (activeEditor == null) return false; + if (newConfig.editor != null && newConfig.editor in activeEditor) return true; if (newConfig.mode != null) { - if (newConfig.mode !== 'result' && editors.markup.isFake) return true; - if (newConfig.mode !== 'codeblock' && editors.markup.codejar) return true; + if (newConfig.mode !== 'result' && activeEditor.isFake) return true; + if (newConfig.mode !== 'codeblock' && activeEditor.codejar) return true; } return false; })(); - if ('configureTailwindcss' in editors.markup) { + const markupEditor = oldConfig?.files.length + ? editors[getMainFile(oldConfig) || 'index.html'] + : editors.markup; + if (markupEditor && 'configureTailwindcss' in markupEditor) { if (newConfig.processors?.includes('tailwindcss')) { - editors.markup.configureTailwindcss?.(true); + markupEditor.configureTailwindcss?.(true); } if ( currentConfig.processors?.includes('tailwindcss') && !newConfig.processors?.includes('tailwindcss') ) { - editors.markup.configureTailwindcss?.(false); + markupEditor.configureTailwindcss?.(false); shouldReloadEditors = true; } } @@ -1558,7 +2122,10 @@ const applyConfig = async (newConfig: Partial, reload = false, oldConfig }; getAllEditors().forEach((editor) => editor.changeSettings(currentEditorConfig)); } - + showEditor(combinedConfig.activeEditor); + if (!isEmbed) { + setTimeout(() => getActiveEditor()?.focus()); + } parent.dispatchEvent(new Event(customEvents.ready)); }; @@ -1605,7 +2172,7 @@ const loadTemplate = async (templateId: string) => { }; const dispatchChangeEvent = debounce(async () => { - let changeEvent: CustomEvent<{ code: Code; config: Config } | void>; + let changeEvent: CustomEvent<{ code: Code; config: SDKConfig } | void>; if (sdkWatchers.code.hasSubscribers()) { if (!cacheIsValid(getCache(), getContentConfig(getConfig()))) { await getResultPage({ forExport: true }); @@ -1613,7 +2180,7 @@ const dispatchChangeEvent = debounce(async () => { changeEvent = new CustomEvent(customEvents.change, { detail: { code: getCachedCode(), - config: getConfig(), + config: getSDKConfig(getConfig()), }, }); } else { @@ -1979,7 +2546,7 @@ const getAllEditors = (): CodeEditor[] => ...Object.values(editors), toolsPane?.console?.getEditor?.(), toolsPane?.compiled?.getEditor?.(), - ].filter((x) => x != null); + ].filter((x) => x != null) as CodeEditor[]; const runViewTransition = (fn: () => void | Promise) => { if ((document as any).startViewTransition) { @@ -2168,6 +2735,7 @@ const showLanguageInfo = async (languageInfo: HTMLElement) => { }; const loadStarterTemplate = async (templateName: Template['name'], checkSaved = true) => { + modal.show(loadingMessage(), { size: 'small' }); const templates = await getTemplates(); const { title, thumbnail, ...templateConfig } = templates.filter((template) => template.name === templateName)?.[0] || {}; @@ -2196,33 +2764,51 @@ const loadStarterTemplate = async (templateName: Template['name'], checkSaved = } }; -const getPlaygroundState = (): Config & Code => { - const config = getConfig(); +const getPlaygroundState = (): Omit & Code => { + const config = getSDKConfig(getConfig()); const cachedCode = getCachedCode(); - return { - ...config, - ...cachedCode, - markup: { - ...config.markup, - ...cachedCode.markup, - position: editors.markup.getPosition(), - }, - style: { - ...config.style, - ...cachedCode.style, - position: editors.style.getPosition(), - }, - script: { - ...config.script, - ...cachedCode.script, - position: editors.script.getPosition(), - }, - tools: { - enabled: config.tools.enabled, - active: toolsPane?.getActiveTool() ?? '', - status: toolsPane?.getStatus() ?? '', - }, + const tools: Config['tools'] = { + enabled: config.tools.enabled, + active: toolsPane?.getActiveTool() ?? '', + status: toolsPane?.getStatus() ?? '', }; + + return 'files' in config && 'files' in cachedCode + ? { + ...getMultiFileConfig(config), + ...cachedCode, + files: cachedCode.files.map((file) => ({ + ...file, + position: editors[file.filename]?.getPosition(), + })), + tools, + } + : 'markup' in config && 'markup' in cachedCode + ? { + ...getSingleFileConfig(config), + ...cachedCode, + markup: { + ...config.markup, + ...cachedCode.markup, + position: editors.markup?.getPosition(), + }, + style: { + ...config.style, + ...cachedCode.style, + position: editors.style?.getPosition(), + }, + script: { + ...config.script, + ...cachedCode.script, + position: editors.script?.getPosition(), + }, + tools, + } + : ({ + ...config, + ...cachedCode, + tools, + } as SDKConfig & Code); }; const zoom = (level: Config['zoom'] = 1) => { @@ -2442,12 +3028,6 @@ const handleTitleEdit = () => { } }; - const removeFormatting = (e: any) => { - e.preventDefault(); - const text = e.clipboardData.getData('text/plain'); - document.execCommand('insertHTML', false, text); - }; - eventsManager.addEventListener(projectTitle, 'input', () => setProjectTitle(), false); eventsManager.addEventListener(projectTitle, 'blur', () => setProjectTitle(true), false); eventsManager.addEventListener(projectTitle, 'keypress', blurOnEnter as any, false); @@ -2549,8 +3129,9 @@ const handleChangeLanguage = () => { } }; -const handleChangeContent = () => { - const contentChanged = async (editorId: EditorId, loading: boolean) => { +const handleChangeContent = (editor?: CodeEditor) => { + const contentChanged = async (editor: CodeEditor, loading: boolean) => { + const editorId = editor.getEditorId(); updateConfig(); const config = getConfig(); addConsoleInputCodeCompletion(); @@ -2559,17 +3140,23 @@ const handleChangeContent = () => { await run(editorId); } - if (config.markup.content !== getCache().markup.content) { + if (getSource(editorId, config)?.content !== getSource(editorId, getCache())?.content) { await getResultPage({ sourceEditor: editorId }); } + const lang = getSource(editorId, config)?.language; + for (const key of Object.keys(customEditors)) { - if (config[editorId].language === key) { + if (lang === key) { await customEditors[key]?.show(true, { baseUrl, editors, config, - html: getCache().markup.compiled || config.markup.content || '', + html: + getCache().markup.compiled || + config.markup.content || + getSource(config.mainFile || getMainFile(config) || 'index.html', config)?.content || + '', eventsManager, }); } @@ -2583,18 +3170,27 @@ const handleChangeContent = () => { loadModuleTypes(editors, config); }; - const debouncecontentChanged = (editorId: EditorId) => + const debouncecontentChanged = (editor: CodeEditor) => debounce( async () => { - await contentChanged(editorId, changingContent); + await contentChanged(editor, changingContent); }, () => getConfig().delay ?? defaultConfig.delay, ); - (Object.keys(editors) as EditorId[]).forEach((editorId) => { - editors[editorId].onContentChanged(debouncecontentChanged(editorId)); - editors[editorId].onContentChanged(setSavedStatus); - }); + const subscribeEditor = (editor: CodeEditor) => { + if (!editor) return; + editor.onContentChanged(debouncecontentChanged(editor)); + editor.onContentChanged(setSavedStatus); + }; + + if (editor) { + subscribeEditor(editor); + } else { + Object.values(editors).forEach((editor) => { + subscribeEditor(editor); + }); + } }; const handleKeyboardShortcutsScreen = () => { @@ -2799,7 +3395,7 @@ const handleI18nMenu = () => { }; const handleEditorTools = () => { - if (!configureEditorTools(getActiveEditor().getLanguage())) return; + if (!configureEditorTools(getActiveEditor()?.getLanguage())) return; const originalMode = getConfig().mode; eventsManager.addEventListener(UI.getFocusButton(), 'click', () => { const config = getConfig(); @@ -2820,7 +3416,8 @@ const handleEditorTools = () => { }); eventsManager.addEventListener(UI.getCopyButton(), 'click', () => { - if (copyToClipboard(getActiveEditor().getValue())) { + const activeEditor = getActiveEditor(); + if (activeEditor && copyToClipboard(activeEditor.getValue())) { notifications.success( window.deps.translateString('core.copy.copied', 'Code copied to clipboard'), ); @@ -2833,14 +3430,14 @@ const handleEditorTools = () => { eventsManager.addEventListener(UI.getUndoButton(), 'click', () => { const activeEditor = getActiveEditor(); - activeEditor.undo(); - activeEditor.focus(); + activeEditor?.undo(); + activeEditor?.focus(); }); eventsManager.addEventListener(UI.getRedoButton(), 'click', () => { const activeEditor = getActiveEditor(); - activeEditor.redo(); - activeEditor.focus(); + activeEditor?.redo(); + activeEditor?.focus(); }); eventsManager.addEventListener(UI.getFormatButton(), 'click', async () => { @@ -2849,9 +3446,11 @@ const handleEditorTools = () => { eventsManager.addEventListener(UI.getCopyAsUrlButton(), 'click', () => { const currentEditor = getActiveEditor(); - const mimeType = 'text/' + currentEditor.getLanguage(); - const dataUrl = toDataUrl(currentEditor.getValue(), mimeType); - if (copyToClipboard(dataUrl)) { + const content = currentEditor?.getValue() || ''; + const language = currentEditor?.getLanguage(); + const mimeType = 'text/' + currentEditor?.getLanguage(); + const dataUrl = language === 'binary' ? content : toDataUrl(content, mimeType); + if (currentEditor && copyToClipboard(dataUrl)) { notifications.success( window.deps.translateString('core.copy.copiedAsDataURL', 'Code copied as data URL'), ); @@ -3289,22 +3888,37 @@ const handleNew = () => { const createTemplatesUI = async () => { initTemplatesSearchIndex(); const starterTemplatesList = UI.getStarterTemplatesList(templatesContainer); - if (!starterTemplatesList) return; + const multifileTemplatesList = UI.getMultifileTemplatesList(templatesContainer); + if (!starterTemplatesList || !multifileTemplatesList) return; starterTemplatesList.innerHTML = ''; + multifileTemplatesList.innerHTML = ''; const searchInput = UI.getTemplatesSearchInput(templatesContainer); if (searchInput) { searchInput.value = ''; } const loadingText = starterTemplatesList?.firstElementChild; + const multifileLoadingText = multifileTemplatesList?.firstElementChild; + const createLink = (template: Template & { id: string }, list: HTMLElement) => { + const link = createStarterTemplateLink(template, list, baseUrl); + eventsManager.addEventListener( + link, + 'click', + (event) => { + event.preventDefault(); + loadStarterTemplate(template.name, /* checkSaved= */ false); + }, + false, + ); + }; getTemplates() - .then((starterTemplates) => { + .then((allTemplates) => { loadingText?.remove(); - starterTemplates.forEach((template, id) => { - const link = createStarterTemplateLink( + multifileLoadingText?.remove(); + allTemplates.forEach((template, id) => { + const link = createLink( { id: String(id), ...template }, - starterTemplatesList, - baseUrl, - ); + template.files?.length ? multifileTemplatesList : starterTemplatesList, + )!; addTemplateToIndex({ id: String(id), ...template }); eventsManager.addEventListener( link, @@ -3319,6 +3933,7 @@ const handleNew = () => { }) .catch(() => { loadingText?.remove(); + multifileLoadingText?.remove(); notifications.error( window.deps.translateString( 'core.error.failedToLoadTemplates', @@ -3326,7 +3941,6 @@ const handleNew = () => { ), ); }); - loadUserTemplates(); requestAnimationFrame(() => UI.getStarterTemplatesTab(templatesContainer)?.click()); modal.show(templatesContainer, { isAsync: true, size: 'large-fixed' }); @@ -3408,7 +4022,7 @@ const handleImport = () => { eventsManager, getUser: authService?.getUser, loadConfig, - populateConfig, + importFromFiles, projectStorage: stores.projects, showScreen, }); @@ -3482,11 +4096,14 @@ const handleExport = () => { await getResultPage({}); } const cache = getCachedCode(); - const compiled = { - markup: cache.markup.compiled, - style: cache.style.compiled, - script: cache.script.compiled, - }; + const compiled = + 'markup' in cache + ? { + markup: cache.markup.compiled, + style: cache.style.compiled, + script: cache.script.compiled, + } + : cache.files.reduce((acc, file) => ({ ...acc, [file.filename]: file.compiled }), {}); await loadModule(); exportModule.exportConfig(getConfig(), baseUrl, 'codepen', { baseUrl, @@ -3509,11 +4126,14 @@ const handleExport = () => { await getResultPage({}); } const cache = getCachedCode(); - const compiled = { - markup: cache.markup.compiled, - style: cache.style.compiled, - script: cache.script.compiled, - }; + const compiled = + 'markup' in cache + ? { + markup: cache.markup.compiled, + style: cache.style.compiled, + script: cache.script.compiled, + } + : cache.files.reduce((acc, file) => ({ ...acc, [file.filename]: file.compiled }), {}); await loadModule(); exportModule.exportConfig(getConfig(), baseUrl, 'jsfiddle', { baseUrl, @@ -4071,7 +4691,8 @@ const handleEmbed = () => { const changeEditorSettings = (newConfig: Partial | null) => { if (!newConfig) return; const shouldReload = - newConfig.editor !== getConfig().editor && !((newConfig.editor || '') in getActiveEditor()); + newConfig.editor !== getConfig().editor && + !((newConfig.editor || '') in (getActiveEditor() || {})); setUserConfig(newConfig); const updatedConfig = getConfig(); @@ -4084,7 +4705,7 @@ const changeEditorSettings = (newConfig: Partial | null) => { }); } showEditorModeStatus(updatedConfig.activeEditor || 'markup'); - getActiveEditor().focus(); + getActiveEditor()?.focus(); }; const handleEditorSettings = () => { @@ -4144,8 +4765,8 @@ const handleCodeToImage = () => { editor: 'codejar', theme: 'dark', wordWrap: true, - language: activeEditor.getLanguage(), - value: activeEditor.getValue(), + language: activeEditor?.getLanguage() || 'html', + value: activeEditor?.getValue() || '', readonly: false, editorId: 'codeToImage', isEmbed: false, @@ -4160,7 +4781,7 @@ const handleCodeToImage = () => { const currentUrl = (location.origin + location.pathname).split('/').slice(0, -1).join('/'); - const getShareUrl = async (config: Partial, shortUrl = true) => { + const getShareUrl = async (config: Partial, shortUrl = true) => { if (shortUrl) { const param = '/?x=id/' + (await shareService.shareProject(config)); return currentUrl + param; @@ -4177,13 +4798,13 @@ const handleCodeToImage = () => { baseUrl, currentUrl, fileName: safeName(fileName, '-').toLowerCase(), - editorId: getLanguageEditorId(activeEditor.getLanguage()) || 'script', + editorId: activeEditor?.getEditorId() || 'script', modal, notifications, eventsManager, deps: { createEditor: createPreviewEditor, - getFormatFn: () => formatter.getFormatFn(activeEditor.getLanguage()), + getFormatFn: () => formatter.getFormatFn(activeEditor?.getLanguage() || 'javascript'), getShareUrl, getSavedPreset, savePreset, @@ -4688,7 +5309,7 @@ const handleResultPopup = () => { } if (ev.data.type === 'ready') { resultPopup?.postMessage( - { result: await getResultPage({ singleFile: true }) }, + { result: await getResultPage({ singleFileResult: true }) }, location.origin, ); } @@ -4777,13 +5398,52 @@ const handleDropFiles = () => { eventsManager.addEventListener(document, 'drop', (event: DragEvent) => { event.preventDefault(); - const files = event.dataTransfer?.files; - if (!files?.length) return; + if (!event.dataTransfer) return; + const files = event.dataTransfer.files; + const items = event.dataTransfer.items; // for directories + if (!files?.length && !items?.length) return; + const entries = { files, items }; + modal.show(loadingMessage(), { size: 'small', autoFocus: false }); - importFromFiles(files, populateConfig, eventsManager) - .then(loadConfig) + importFromFiles(entries, /* multiFile= */ true) + .then(async (fileConfig) => { + // if in single file project, load as a new project + // otherwise, add files to current project + const currentConfig = getConfig(); + if (Object.keys(fileConfig).length === 0) return; + if (!currentConfig.files.length) { + checkSavedAndExecute(async () => { + await loadConfig(fileConfig); + modal.close(); + })(); + } else { + for (const file of fileConfig.files || []) { + if (currentConfig.files.find((f) => f.filename === file.filename)) { + editors[file.filename]?.setValue(file.content); + } else { + await addFile(file.filename, { + baseUrl, + mode: currentConfig.mode, + readonly: currentConfig.readonly, + ...getEditorConfig(currentConfig), + isEmbed, + isLite, + isHeadless, + mapLanguage, + getLanguageExtension, + getFormatterConfig: () => getFormatterConfig(currentConfig), + getFontFamily, + }); + editors[file.filename]?.setValue(file.content); + } + } + showEditor(fileConfig.files?.[0].filename); + modal.close(); + } + }) .catch((message) => { notifications.error(message); + modal.close(); }); }); @@ -4986,7 +5646,7 @@ const basicHandlers = () => { isEmbed, onClose: () => { if (!isEmbed) { - getActiveEditor().focus(); + getActiveEditor()?.focus(); } }, }); @@ -4999,7 +5659,6 @@ const basicHandlers = () => { handleIframeScroll(); handleSelectEditor(); handleChangeLanguage(); - handleChangeContent(); // Setup keyboard shortcuts with dependency injection handleKeyboardShortcuts({ eventsManager, @@ -5136,7 +5795,7 @@ const configureModes = ({ const importExternalContent = async (options: { config?: Config; - sdkConfig?: Partial; + sdkConfig?: Partial; configUrl?: string; template?: string; importUrl?: string; @@ -5146,8 +5805,9 @@ const importExternalContent = async (options: { const hasContentUrls = (conf: Partial) => editorIds.filter( (editorId) => - (conf[editorId]?.contentUrl && !conf[editorId]?.content) || - (conf[editorId]?.hiddenContentUrl && !conf[editorId]?.hiddenContent), + (editorId === 'markup' || editorId === 'style' || editorId === 'script') && + ((conf[editorId]?.contentUrl && !conf[editorId]?.content) || + (conf[editorId]?.hiddenContentUrl && !conf[editorId]?.hiddenContent)), ).length > 0; const validConfigUrl = getValidUrl(configUrl); if (importUrl?.startsWith('config') || importUrl?.startsWith('params')) { @@ -5156,11 +5816,10 @@ const importExternalContent = async (options: { if (!validConfigUrl && !template && !importUrl && !hasContentUrls(config)) return false; - const loadingMessage = window.deps.translateString('core.import.loading', 'Loading Project...'); - notifications.info(loadingMessage); + modal.show(loadingMessage(), { size: 'small' }); let templateConfig: Partial = {}; - let importUrlConfig: Partial = {}; + let importUrlConfig: Partial = {}; let contentUrlConfig: Partial = {}; let configUrlConfig: Partial = {}; @@ -5180,6 +5839,7 @@ const importExternalContent = async (options: { ); } } + if (importUrl) { let validImportUrl = importUrl; if (importUrl.startsWith('http') || importUrl.startsWith('data')) { @@ -5210,27 +5870,30 @@ const importExternalContent = async (options: { // load content from config contentUrl const editorsContent = await Promise.all( editorIds.map(async (editorId) => { - const contentUrl = config[editorId].contentUrl; - const hiddenContentUrl = config[editorId].hiddenContentUrl; + if (!isEditorId(editorId)) return; + const src = config[editorId]; + const contentUrl = src.contentUrl; + const hiddenContentUrl = src.hiddenContentUrl; const [content, hiddenContent] = await Promise.all([ - contentUrl && getValidUrl(contentUrl) && !config[editorId].content + contentUrl && getValidUrl(contentUrl) && !src.content ? fetch(contentUrl).then((res) => res.text()) : Promise.resolve(''), - hiddenContentUrl && getValidUrl(hiddenContentUrl) && !config[editorId].hiddenContent + hiddenContentUrl && getValidUrl(hiddenContentUrl) && !src.hiddenContent ? fetch(hiddenContentUrl).then((res) => res.text()) : Promise.resolve(''), ]); return { - ...config[editorId], + ...src, ...(content ? { content } : {}), ...(hiddenContent ? { hiddenContent } : {}), }; }), ); + // TODO: handle files contentUrlConfig = { - markup: editorsContent[0], - style: editorsContent[1], - script: editorsContent[2], + markup: editorsContent[0] || config.markup, + style: editorsContent[1] || config.style, + script: editorsContent[2] || config.script, }; } @@ -5253,11 +5916,12 @@ const importExternalContent = async (options: { ...configUrlConfig, ...sdkConfig, ...contentUrlConfig, - }), + } as Partial), parent.location.href, false, ); + modal.close(); loadSelectedScreen(); return true; @@ -5310,7 +5974,7 @@ const loadDefaults = async () => { const initializePlayground = async ( options?: { - config?: Partial; + config?: Partial; baseUrl?: string; isEmbed?: boolean; isHeadless?: boolean; @@ -5339,12 +6003,14 @@ const initializePlayground = async ( window.history.replaceState(null, '', './'); // fix URL from "/app" to "/" await initializeStores(stores, isEmbed); const userConfig = stores.userConfig?.getValue() ?? {}; - setConfig(buildConfig({ ...getConfig(), ...userConfig, ...initialConfig })); + setConfig(buildConfig({ ...getConfig(), ...userConfig, ...initialConfig } as Partial)); configureModes({ config: getConfig(), isEmbed, isLite }); compiler = (window as any).compiler = await getCompiler({ config: getConfig(), baseUrl, eventsManager, + getTypes: async (code: string) => + typeLoader.load(code, { ...getConfig().types, ...getConfig().customSettings.types }, true), }); formatter = getFormatter(getConfig(), baseUrl, isEmbed); customEditors = createCustomEditors({ baseUrl, eventsManager }); @@ -5388,46 +6054,54 @@ const initializePlayground = async ( const createApi = (): API => { const apiGetShareUrl = async (shortUrl = false) => (await share(shortUrl, true, false)).url; - const apiGetConfig = async (contentOnly = false): Promise => { + const apiGetConfig = async (contentOnly = false): Promise => { updateConfig(); const config = contentOnly ? getContentConfig(getConfig()) : getConfig(); - return JSON.parse(JSON.stringify(config)); + return getSDKConfig(config); }; - const apiSetConfig = async (newConfig: Partial): Promise => { + const apiSetConfig = async (newConfig: Partial): Promise => { const currentConfig = getConfig(); - const newAppConfig = buildConfig({ ...currentConfig, ...newConfig }); + const newAppConfig = buildConfig({ + ...currentConfig, + ...(newConfig as Partial), + // allow changing multifile project to singlefile + ...(currentConfig.files.length && + !newConfig.files?.length && + (newConfig.markup?.language || newConfig.style?.language || newConfig.script?.language) + ? { files: [] } + : {}), + }); const hasNewAppLanguage = newConfig.appLanguage && newConfig.appLanguage !== i18n?.getLanguage(); const shouldRun = newConfig.mode != null && newConfig.mode !== 'editor' && newConfig.mode !== 'codeblock'; const shouldReloadCompiler = shouldRun && compiler.isFake; - const isContentOnlyChange = compareObjects( - newConfig, - currentConfig as Record, - ).every((k) => ['markup.content', 'style.content', 'script.content'].includes(k)); + const isContentOnlyChange = + !currentConfig.files.length && + !newConfig.files?.length && + compareObjects(newConfig, currentConfig as Record).every((k) => + ['markup.content', 'style.content', 'script.content'].includes(k), + ); setConfig(newAppConfig); if (isContentOnlyChange) { for (const key of ['markup', 'style', 'script'] as const) { - const content = newConfig[key]?.content; + const content = (newAppConfig as Partial)[key]?.content; if (content != null) { editors[key].setValue(content); } } - return newAppConfig; - } - - if (hasNewAppLanguage) { - changeAppLanguage(newConfig.appLanguage!); - return newAppConfig; - } - if (shouldReloadCompiler) { + } else if (hasNewAppLanguage) { + changeAppLanguage(newAppConfig.appLanguage!); + } else if (shouldReloadCompiler) { await reloadCompiler(newAppConfig); + } else { + await applyConfig(newAppConfig as Partial, /* reload = */ true, currentConfig); } - await applyConfig(newConfig, /* reload = */ true, currentConfig); - return newAppConfig; + + return getSDKConfig(newAppConfig); }; const apiGetCode = async (): Promise => { @@ -5435,7 +6109,7 @@ const createApi = (): API => { if (!cacheIsValid(getCache(), getContentConfig(getConfig()))) { await getResultPage({ forExport: true }); } - return JSON.parse(JSON.stringify(getCachedCode())); + return cloneObject(getCachedCode()); }; const apiShow: API['show'] = async ( @@ -5459,7 +6133,7 @@ const createApi = (): API => { split?.show('code', full); } else if (panel === 'console' || panel === 'compiled' || panel === 'tests') { split?.show('output'); - toolsPane?.setActiveTool(panel); + toolsPane?.setActiveTool(panel as 'console' | 'compiled' | 'tests'); if (full) { toolsPane?.maximize(); } else { @@ -5470,8 +6144,8 @@ const createApi = (): API => { split?.show('code', full); if (typeof line === 'number' && line > 0) { const col = typeof column === 'number' && column > -1 ? column : 0; - getActiveEditor().setPosition({ lineNumber: line, column: col }); - getActiveEditor().focus(); + getActiveEditor()?.setPosition({ lineNumber: line, column: col }); + getActiveEditor()?.focus(); } } else { throw new Error(window.deps.translateString('core.error.invalidPanelId', 'Invalid panel id')); @@ -5573,7 +6247,7 @@ const createApi = (): API => { }; }; -const initApp = async (config: Partial, baseUrl: string) => { +const initApp = async (config: Partial, baseUrl: string) => { window.deps = { showMode, translateString: translateStringMock, @@ -5588,7 +6262,7 @@ const initApp = async (config: Partial, baseUrl: string) => { return createApi(); }; -const initEmbed = async (config: Partial, baseUrl: string) => { +const initEmbed = async (config: Partial, baseUrl: string) => { window.deps = { showMode, translateString: translateStringMock, @@ -5604,7 +6278,7 @@ const initEmbed = async (config: Partial, baseUrl: string) => { return createApi(); }; -const initHeadless = async (config: Partial, baseUrl: string) => { +const initHeadless = async (config: Partial, baseUrl: string) => { window.deps = { showMode: () => undefined, translateString: translateStringMock, diff --git a/src/livecodes/editor/binary-file-editor.ts b/src/livecodes/editor/binary-file-editor.ts new file mode 100644 index 0000000000..046e0029e4 --- /dev/null +++ b/src/livecodes/editor/binary-file-editor.ts @@ -0,0 +1,120 @@ +import type { CodeEditor, EditorOptions } from '../models'; +import { createFakeEditor } from './fake-editor'; + +export const createBinaryFileEditor = (options: EditorOptions): CodeEditor => { + if (!options.container) return createFakeEditor(options); + const container = document.createElement('div'); + container.classList.add('binary-file-editor'); + options.container.appendChild(container); + + const editor = createFakeEditor(options); + + type Listener = () => void; + const listeners: Listener[] = []; + const onContentChanged = (fn: Listener) => { + listeners.push(fn); + }; + + const setValue = (value: string = '') => { + editor.setValue(value); + listeners.forEach((fn) => fn()); + if (value) { + showFile(); + } else { + showFileSelector(); + } + }; + + const fileSelector = document.createElement('input'); + fileSelector.type = 'file'; + fileSelector.onchange = (ev) => { + const file = (ev.target as HTMLInputElement).files?.[0]; + if (!file) return; + + // Max 2 MB allowed + const maxSizeAllowed = 2 * 1024 * 1024; + if (file.size > maxSizeAllowed) { + alert( + window.deps.translateString( + 'generic.error.exceededSize', + 'Error: Exceeded size {{size}} MB', + { size: 2 }, + ), + ); + return; + } + + const reader = new FileReader(); + reader.onload = () => { + setValue(reader.result as string); + showFile(); + }; + reader.readAsDataURL(file); + }; + + const showFileSelector = () => { + const btn = document.createElement('button'); + btn.innerHTML = window.deps.translateString('app.binaryFileEditor.selectFile', 'Select file'); + btn.onclick = () => fileSelector.click(); + container.innerHTML = ''; + container.appendChild(btn); + }; + + const showFile = () => { + const src = editor.getValue(); + + if (!src) { + showFileSelector(); + return; + } + + let display; + if (src.startsWith('data:video')) { + display = document.createElement('video'); + display.src = src; + } else { + display = document.createElement('img'); + if (src.startsWith('data:image')) { + display.src = src; + } else if (src.startsWith('data:audio')) { + display.src = options.baseUrl + 'assets/images/audio.svg'; + display.classList.add('icon'); + } else if (src.startsWith('data:font')) { + display.src = options.baseUrl + 'assets/images/font.svg'; + display.classList.add('icon'); + } else { + display.src = options.baseUrl + 'assets/images/file.svg'; + display.classList.add('icon'); + } + } + + const link = document.createElement('a'); + link.title = window.deps.translateString('app.binaryFileEditor.selectFile', 'Select file'); + link.onclick = (ev) => { + ev.preventDefault(); + fileSelector.click(); + }; + link.appendChild(display); + + container.innerHTML = ''; + container.appendChild(link); + }; + + if (!editor.getValue()) { + showFileSelector(); + } else { + showFile(); + } + + const destroy = () => { + listeners.length = 0; + container.remove(); + }; + + return { + ...editor, + setValue, + onContentChanged, + destroy, + }; +}; diff --git a/src/livecodes/editor/codejar/codejar.ts b/src/livecodes/editor/codejar/codejar.ts index 182496bfec..383ec5ab97 100644 --- a/src/livecodes/editor/codejar/codejar.ts +++ b/src/livecodes/editor/codejar/codejar.ts @@ -10,6 +10,7 @@ import 'prismjs/components/prism-typescript'; import 'prismjs/plugins/autoloader/prism-autoloader'; import 'prismjs/plugins/line-numbers/prism-line-numbers'; +import { getFileLanguage } from '../../languages'; import type { CodeEditor, CodejarTheme, @@ -32,13 +33,17 @@ Prism.manual = true; Prism.plugins.autoloader.languages_path = prismBaseUrl; export const createEditor = async (options: EditorOptions): Promise => { - const { container, mode, editorId, readonly, isEmbed, getFormatterConfig, getFontFamily } = - options; + const { container, mode, readonly, isEmbed, getFormatterConfig, getFontFamily } = options; if (!container) throw new Error('editor container not found'); - let { value, language } = options; + let { value, language, editorId } = options; let currentPosition: EditorPosition = { lineNumber: 1 }; - const mapLanguage = (lang: Language) => options.mapLanguage?.(lang, 'codejar'); + const mapLanguage = (lang: Language) => { + if (!lang) return 'html'; + if (editorId.endsWith('.js')) return 'javascript'; + if (editorId.endsWith('.ts')) return 'typescript'; + return options.mapLanguage?.(lang, 'codejar') || lang; + }; let mappedLanguage = mapLanguage(language); let editorOptions: ReturnType; @@ -135,6 +140,13 @@ export const createEditor = async (options: EditorOptions): Promise // codejar?.onPaste(handleUpdate); const getEditorId = () => editorId; + const setEditorId = (filename: string, lang?: Language) => { + editorId = filename; + const newLang = lang || getFileLanguage(filename, {}); + if (newLang && newLang !== language) { + setLanguage(newLang); + } + }; const getValue = () => (codejar ? codejar.toString() : value); const setValue = (newValue = '\n') => { value = newValue; @@ -157,6 +169,7 @@ export const createEditor = async (options: EditorOptions): Promise const getLanguage = () => language; const setLanguage = (lang: Language, newValue?: string) => { + if (!lang) return; language = lang; mappedLanguage = mapLanguage(language); codeElement.className = 'language-' + mappedLanguage; @@ -427,6 +440,7 @@ export const createEditor = async (options: EditorOptions): Promise getLanguage, setLanguage, getEditorId, + setEditorId, focus, getPosition, setPosition: (position) => setPosition(position), diff --git a/src/livecodes/editor/codemirror/codemirror.ts b/src/livecodes/editor/codemirror/codemirror.ts index 4e1543029d..ccaf3fba27 100644 --- a/src/livecodes/editor/codemirror/codemirror.ts +++ b/src/livecodes/editor/codemirror/codemirror.ts @@ -29,7 +29,7 @@ import { colorPicker } from '@replit/codemirror-css-color-picker'; // these are imported normally import { getEditorModeNode } from '../../UI/selectors'; -import { getLanguageSpecs } from '../../languages'; +import { getFileLanguage, getLanguageSpecs } from '../../languages'; import type { CodeEditor, CodemirrorTheme, @@ -46,7 +46,6 @@ import { ctrl, debounce, getRandomString } from '../../utils/utils'; import { codeMirrorBaseUrl, codemirrorMinimapUrl, comlinkBaseUrl } from '../../vendors'; import { getEditorTheme } from '../themes'; import { codemirrorThemes, customThemes } from './codemirror-themes'; -import { editorLanguages } from './editor-languages'; // export type CodeiumEditor = Pick & { // editorId: EditorOptions['editorId']; @@ -57,23 +56,15 @@ let tabFocusMode = false; const changeTabFocusMode = debounce(() => (tabFocusMode = !tabFocusMode), 50); export const createEditor = async (options: EditorOptions): Promise => { - const { - container, - readonly, - isEmbed, - editorId, - getFormatterConfig, - getFontFamily, - getLanguageExtension, - } = options; + const { container, readonly, isEmbed, getFormatterConfig, getFontFamily, getLanguageExtension } = + options; + let { editorId, language } = options; let editorSettings: EditorConfig = { ...options }; if (!container) throw new Error('editor container not found'); const getLanguageSupport = async (lang: Language): Promise => { const langSupport = getLanguageSpecs(lang)?.editorSupport?.codemirror?.languageSupport; - if (!langSupport) { - return editorLanguages[lang]?.() || editorLanguages.html?.() || []; - } + if (!langSupport) return []; const loadLanguage: () => Promise = typeof langSupport === 'string' ? (await import(langSupport)).default @@ -83,10 +74,10 @@ export const createEditor = async (options: EditorOptions): Promise return loadLanguage(); }; - const mapLanguage = (lang: Language) => { - if (lang.startsWith('vue')) return 'vue'; - if (lang.startsWith('svelte')) return 'svelte'; - if (lang === 'liquid') return 'liquid'; + const mapLanguage = (lang: Language | undefined) => { + if (!lang) return 'html'; + if (editorId.endsWith('.ts')) return 'typescript'; + if (editorId.endsWith('.js')) return 'javascript'; return options.mapLanguage?.(lang, 'codemirror') || lang; }; @@ -103,7 +94,6 @@ export const createEditor = async (options: EditorOptions): Promise const defaultThemes: Record = { dark: 'one-dark', light: 'cm-light' }; const getActiveTheme = () => themes[theme] || themes[defaultThemes[options.theme]] || []; - let language = options.language; let mappedLanguage = mapLanguage(language); let mappedLanguageSupport = await getLanguageSupport(mappedLanguage); let theme: CodemirrorTheme = await loadTheme(options.theme, options.editorTheme); @@ -171,7 +161,7 @@ export const createEditor = async (options: EditorOptions): Promise mappedLanguage === 'typescript' && !ext?.endsWith('ts') && !ext?.endsWith('tsx') ? ext + '.tsx' : ext; - const path = `/${editorId}.${random}.${extension}`; + const path = editorId.includes('.') ? `/${editorId}` : `/${editorId}.${random}.${extension}`; codemirrorTS = codemirrorTS || [ tsFacetWorker.of({ worker: tsWorker, path }), @@ -333,6 +323,14 @@ export const createEditor = async (options: EditorOptions): Promise showEditorMode(options.editorMode); const getEditorId = () => editorId; + const setEditorId = (filename: string, lang?: Language) => { + editorId = filename; + const newLang = lang || getFileLanguage(filename, {}); + if (newLang && newLang !== language) { + setLanguage(newLang); + } + tsLoaded.then(() => loadTS(true)); + }; const getValue = () => view.state.doc.toString(); const setValue = (value = '', newState = true) => { if (newState) { @@ -350,6 +348,7 @@ export const createEditor = async (options: EditorOptions): Promise const focus = () => view.focus(); const getLanguage = () => language; const setLanguage = (lang: Language, value?: string) => { + if (!lang) return; language = lang; mappedLanguage = mapLanguage(language); getLanguageSupport(mappedLanguage).then((langSupport) => { @@ -358,9 +357,7 @@ export const createEditor = async (options: EditorOptions): Promise effects: [languageExtension.reconfigure(mappedLanguageSupport)], }); }); - tsLoaded.then(() => { - loadTS(true); - }); + tsLoaded.then(() => loadTS(true)); if (value != null) { setValue(value); } @@ -552,6 +549,7 @@ export const createEditor = async (options: EditorOptions): Promise getLanguage, setLanguage, getEditorId, + setEditorId, focus, getPosition, setPosition, diff --git a/src/livecodes/editor/codemirror/editor-languages.ts b/src/livecodes/editor/codemirror/editor-languages.ts deleted file mode 100644 index b921ceb36b..0000000000 --- a/src/livecodes/editor/codemirror/editor-languages.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable import/no-unresolved */ -// @ts-ignore -import type { LanguageSupport } from '@codemirror/language'; -// @ts-ignore -import { json } from '@codemirror/lang-json'; - -import type { Language } from '../../models'; - -export const editorLanguages: Partial<{ [key in Language]: () => Promise }> = { - json: async () => json(), -}; diff --git a/src/livecodes/editor/create-editor.ts b/src/livecodes/editor/create-editor.ts index 5fa27664b0..85dffde055 100644 --- a/src/livecodes/editor/create-editor.ts +++ b/src/livecodes/editor/create-editor.ts @@ -1,5 +1,7 @@ +import { getFileLanguage } from '../languages/utils'; import type { CodeEditor, Config, EditorOptions } from '../models'; import { isMobile, loadStylesheet } from '../utils'; +import { createBinaryFileEditor } from './binary-file-editor'; import { createFakeEditor } from './fake-editor'; import { fonts } from './fonts'; @@ -30,21 +32,23 @@ const selectEditor = (options: EditorOptions & { activeEditor?: Config['activeEd const { editor, mode, editorId, activeEditor, isLite, isHeadless } = options; const auto = isMobile() ? 'codemirror' : 'monaco'; return ( - (isHeadless - ? 'fake' - : mode === 'result' && editorId !== 'console' && editorId !== 'compiled' + (getFileLanguage(editorId, {}) === 'binary' + ? 'binary' + : isHeadless ? 'fake' - : mode === 'simple' && editorId !== activeEditor + : mode === 'result' && editorId !== 'console' && editorId !== 'compiled' ? 'fake' - : ['codemirror', 'monaco', 'codejar'].includes(editor || '') - ? editor - : editor === 'auto' - ? auto - : mode === 'simple' && editorId === activeEditor - ? 'codemirror' - : mode === 'codeblock' || isLite - ? 'codejar' - : auto) || 'monaco' + : mode === 'simple' && editorId !== activeEditor + ? 'fake' + : ['codemirror', 'monaco', 'codejar'].includes(editor || '') + ? editor + : editor === 'auto' + ? auto + : mode === 'simple' && editorId === activeEditor + ? 'codemirror' + : mode === 'codeblock' || isLite + ? 'codejar' + : auto) || 'monaco' ); }; @@ -93,9 +97,11 @@ export const createEditor = async ( if (!options) throw new Error(); const editorOptions = getEditorOptions(options); + if (!options.language) return createFakeEditor(editorOptions); const editorName = selectEditor(editorOptions); if (editorName === 'fake') return createFakeEditor(editorOptions); + if (editorName === 'binary') return createBinaryFileEditor(editorOptions); if (editorOptions.fontFamily) { loadFont(editorOptions.fontFamily); diff --git a/src/livecodes/editor/fake-editor.ts b/src/livecodes/editor/fake-editor.ts index ef354afcf3..862a6ffbd4 100644 --- a/src/livecodes/editor/fake-editor.ts +++ b/src/livecodes/editor/fake-editor.ts @@ -1,9 +1,8 @@ -import { getLanguageEditorId } from '../languages'; +import { getFileLanguage } from '../languages'; import type { CodeEditor, EditorOptions } from '../models'; export const createFakeEditor = (options: EditorOptions): CodeEditor => { - let value = options.value; - let language = options.language; + let { value, language, editorId } = options; return { getValue: () => value, setValue: (v = '') => { @@ -16,7 +15,11 @@ export const createFakeEditor = (options: EditorOptions): CodeEditor => { value = v; } }, - getEditorId: () => getLanguageEditorId(language) || 'markup', + getEditorId: () => editorId, + setEditorId: (fileName) => { + editorId = fileName; + language = getFileLanguage(fileName, {}) || language; + }, focus: () => undefined, getPosition: () => ({ lineNumber: 1, column: 1 }), setPosition: () => undefined, diff --git a/src/livecodes/editor/monaco/monaco-languages.ts b/src/livecodes/editor/monaco/monaco-languages.ts new file mode 100644 index 0000000000..5135d08be5 --- /dev/null +++ b/src/livecodes/editor/monaco/monaco-languages.ts @@ -0,0 +1,38 @@ +import type * as Monaco from 'monaco-editor'; + +import type { Language } from '../../models'; +import { monacoLanguagesBaseUrl } from '../../vendors'; + +export interface CustomLanguageDefinition { + config?: Monaco.languages.LanguageConfiguration; + tokens?: Monaco.languages.IMonarchLanguage; + completions?: Monaco.languages.CompletionItemProvider; + definitions?: Monaco.languages.DefinitionProvider; + init?: (monaco: typeof Monaco) => void; +} + +export const customLanguages: Partial> = { + astro: monacoLanguagesBaseUrl + 'astro.js', + clio: monacoLanguagesBaseUrl + 'clio.js', + imba: monacoLanguagesBaseUrl + 'imba.js', + json5: monacoLanguagesBaseUrl + 'json5.js', + minizinc: monacoLanguagesBaseUrl + 'minizinc.js', + prolog: monacoLanguagesBaseUrl + 'prolog.js', + // sql: monacoLanguagesBaseUrl + 'sql.js', // TODO: add autocomplete + vue: monacoLanguagesBaseUrl + 'vue.js', + svelte: monacoLanguagesBaseUrl + 'svelte.js', + wat: monacoLanguagesBaseUrl + 'wat.js', + + dotenv: { + config: { comments: { lineComment: '#' } }, + tokens: { + tokenizer: { + root: [ + [/#.*$/, 'comment'], + [/([a-zA-Z_][a-zA-Z0-9_]*)(=)([^#]*)(#.*)$/, ['key', 'delimiter', 'value', 'comment']], + [/([a-zA-Z_][a-zA-Z0-9_]*)(=)(.*)$/, ['key', 'delimiter', 'value']], + ], + }, + }, + }, +}; diff --git a/src/livecodes/editor/monaco/monaco.ts b/src/livecodes/editor/monaco/monaco.ts index 0cda24d149..72ef1f0750 100644 --- a/src/livecodes/editor/monaco/monaco.ts +++ b/src/livecodes/editor/monaco/monaco.ts @@ -1,8 +1,9 @@ import type * as Monaco from 'monaco-editor'; -import { getEditorModeNode } from '../../UI/selectors'; +import { getEditorModeNode, getEditorTab } from '../../UI/selectors'; import { getImports } from '../../compiler/import-map'; import { getLanguageSpecs, hasJsx } from '../../languages'; +import { getFileLanguage } from '../../languages/utils'; import type { APIError, CodeEditor, @@ -52,14 +53,20 @@ export const createEditor = async (options: EditorOptions): Promise editorTheme, isEmbed, getLanguageExtension, - mapLanguage, getFormatterConfig, getFontFamily, } = options; - let language = options.language; + let { editorId, language } = options; if (!container) throw new Error('editor container not found'); + const mapLanguage = (lang: Language | undefined) => { + if (!lang) return 'html'; + if (editorId.endsWith('.ts')) return 'typescript'; + if (editorId.endsWith('.js')) return 'javascript'; + return options.mapLanguage?.(lang, 'monaco') || lang; + }; + const loadMonaco = () => import(monacoBaseUrl + 'monaco.js'); let editorMode: any | undefined; @@ -186,7 +193,6 @@ export const createEditor = async (options: EditorOptions): Promise ...consoleOptions, }; - const editorId = options.editorId; const initOptions = editorId === 'console' ? consoleOptions @@ -202,10 +208,7 @@ export const createEditor = async (options: EditorOptions): Promise const configureTypeScriptFeatures = () => { const JSLangs = ['javascript', 'jsx', 'react', 'flow', 'solid', 'react-native']; const isJSLang = JSLangs.includes(language); - if ( - !['script', 'tests', 'editorSettings'].includes(editorId) || - !['javascript', 'typescript'].includes(mapLanguage(language, 'monaco')) - ) { + if (!['javascript', 'typescript'].includes(mapLanguage(language))) { return; } @@ -257,7 +260,7 @@ export const createEditor = async (options: EditorOptions): Promise const getOrCreateModel = (value: string, lang: string | undefined, uri: Monaco.Uri) => { const model = monaco.editor.getModel(uri); if (model) { - if (model.getLanguageId() === mapLanguage(lang as Language, 'monaco')) { + if (model.getLanguageId() === mapLanguage(lang as Language)) { model.setValue(value); return model; } @@ -275,20 +278,14 @@ export const createEditor = async (options: EditorOptions): Promise const random = getRandomString(); const ext = getLanguageExtension(language); const extension = - mapLanguage(language, 'monaco') === 'typescript' && - !ext?.endsWith('ts') && - !ext?.endsWith('tsx') + mapLanguage(language) === 'typescript' && !ext?.endsWith('ts') && !ext?.endsWith('tsx') ? ext + '.tsx' : ext; modelUri = editorId.includes('.') ? `file:///${editorId}` : `file:///${editorId}.${random}.${extension}`; const oldModel = editor.getModel(); - const model = getOrCreateModel( - value || '', - mapLanguage(language, 'monaco'), - monaco.Uri.parse(modelUri), - ); + const model = getOrCreateModel(value || '', mapLanguage(language), monaco.Uri.parse(modelUri)); editor.setModel(model); setTimeout(() => oldModel?.dispose(), 1000); // avoid race https://github.com/microsoft/monaco-editor/issues/1715 updateListeners(); @@ -297,9 +294,9 @@ export const createEditor = async (options: EditorOptions): Promise const editor = monaco.editor.create(container, { ...editorOptions, - language: mapLanguage(language, 'monaco'), + language: mapLanguage(language), }); - setModel(editor, options.value, mapLanguage(language, 'monaco')); + setModel(editor, options.value, mapLanguage(language)); if (editorId.includes('.')) { editors.push(editor); @@ -317,6 +314,11 @@ export const createEditor = async (options: EditorOptions): Promise } const getEditorId = () => editorId; + const setEditorId = (filename: string, lang?: Language) => { + editorId = filename; + language = lang || getFileLanguage(filename, {}) || language; + setModel(editor, editor.getValue(), language); + }; const getValue = () => editor.getValue(); const setValue = (value = '') => { editor.getModel()?.setValue(value); @@ -504,7 +506,7 @@ export const createEditor = async (options: EditorOptions): Promise const registerFormatter = (formatFn: FormatFn | undefined) => { if (!formatFn) return; - monaco.languages.registerDocumentFormattingEditProvider(mapLanguage(language, 'monaco'), { + monaco.languages.registerDocumentFormattingEditProvider(mapLanguage(language), { provideDocumentFormattingEdits: async (model) => { if (!model || model.isDisposed()) return []; const val = model.getValue() || ''; @@ -681,8 +683,8 @@ export const createEditor = async (options: EditorOptions): Promise const model = editor.getModel(); if ( !model || - !addCloseLanguages.includes(mapLanguage(language, 'monaco')) || - (mapLanguage(language, 'monaco') === 'typescript' && !hasJsx(language)) || // avoid autocompleting TS generics + !addCloseLanguages.includes(mapLanguage(language)) || + (mapLanguage(language) === 'typescript' && !hasJsx(language)) || // avoid autocompleting TS generics editorOptions.autoClosingBrackets === 'never' ) { return; @@ -871,8 +873,8 @@ export const createEditor = async (options: EditorOptions): Promise const targetEditorId = resource.path.slice(1); // remove leading slash if (targetEditorId) { - // const targetEditorTab = getEditorTab(targetEditorId); - // targetEditorTab?.click(); + const targetEditorTab = getEditorTab(targetEditorId); + targetEditorTab?.click(); if (monaco.Range.isIRange(selectionOrPosition)) { targetEditor?.revealRangeInCenterIfOutsideViewport( @@ -908,6 +910,7 @@ export const createEditor = async (options: EditorOptions): Promise getLanguage, setLanguage, getEditorId, + setEditorId, focus, getPosition, setPosition, diff --git a/src/livecodes/export/export-src.ts b/src/livecodes/export/export-src.ts index 7e4c085ff0..4159d01a91 100644 --- a/src/livecodes/export/export-src.ts +++ b/src/livecodes/export/export-src.ts @@ -1,4 +1,4 @@ -import type { getLanguageExtension as getLanguageExtensionFn } from '../languages'; +import { getFileLanguage, type getLanguageExtension as getLanguageExtensionFn } from '../languages'; import type { Config, EditorId } from '../models'; import { downloadFile, loadScript } from '../utils/utils'; import { jsZipUrl } from '../vendors'; @@ -22,7 +22,12 @@ export const exportSrc = async ( const files = getFilesFromConfig(config, deps); (Object.keys(files) as EditorId[]).forEach((filename) => { - zip.file(filename, files[filename]?.content); + const content = files[filename]?.content || ''; + if (getFileLanguage(filename, config) === 'binary') { + zip.file(filename, content.split('base64,')[1] || '', { base64: true }); + } else { + zip.file(filename, content); + } }); zip.file('result.html', html); zip.file('livecodes.json', JSON.stringify(config, null, 2)); diff --git a/src/livecodes/export/utils.ts b/src/livecodes/export/utils.ts index b08352c5f6..32e516246e 100644 --- a/src/livecodes/export/utils.ts +++ b/src/livecodes/export/utils.ts @@ -1,4 +1,5 @@ import { replaceImports } from '../compiler/import-map'; +import { getSource, isEditorId } from '../config/utils'; import type { getLanguageCompiler as getLanguageCompilerFn, getLanguageExtension as getLanguageExtensionFn, @@ -22,16 +23,28 @@ export const getFilesFromConfig = ( style: 'style', script: 'script', }; - const codeFiles = (Object.keys(filenames) as EditorId[]).reduce((files, editorId) => { - const filename = filenames[editorId]; - const language = config[editorId].language; - const extension = getLanguageExtension?.(language) || 'md'; - const content = config[editorId].content || ''; - return { - ...files, - ...(content ? { [filename + '.' + extension]: { content } } : {}), - }; - }, {}); + const codeFiles = + config.files.length > 0 + ? config.files.reduce( + (files, file) => ({ + ...files, + [file.filename]: { content: file.content }, + }), + {}, + ) + : (Object.keys(filenames) as EditorId[]).reduce((files, editorId) => { + if (!isEditorId(editorId)) { + return files; + } + const filename = filenames[editorId]; + const language = config[editorId].language; + const extension = getLanguageExtension?.(language) || 'md'; + const content = config[editorId].content || ''; + return { + ...files, + ...(content ? { [filename + '.' + extension]: { content } } : {}), + }; + }, {}); const externalStyles = config.stylesheets.length > 0 @@ -106,10 +119,10 @@ export const getCompilerScripts = ({ supportedLanguages: { [key in EditorId]: Language[] }; getLanguageCompiler: typeof getLanguageCompilerFn; }) => { - if (supportedLanguages[editorId].includes(config[editorId].language)) return []; - const compilerScripts = getLanguageCompiler?.(config[editorId].language)?.scripts; - const compiledCode = - config[editorId].language === 'python' ? config[editorId].content || '' : compiled[editorId]; + const src = getSource(editorId, config); + if (!src || supportedLanguages[editorId].includes(src.language)) return []; + const compilerScripts = getLanguageCompiler?.(src.language)?.scripts; + const compiledCode = src.language === 'python' ? src.content || '' : compiled[editorId]; const scripts = typeof compilerScripts === 'function' ? compilerScripts({ compiled: compiledCode, baseUrl, config }) @@ -126,7 +139,7 @@ export const getContent = ({ }: { editorId: EditorId; config: Config; - compiled: { [key in EditorId]: string }; + compiled: Partial<{ [key in EditorId]: string }>; supportedLanguages: { [key in EditorId]: Language[] }; getLanguageCompiler: typeof getLanguageCompilerFn; }) => { @@ -137,19 +150,23 @@ export const getContent = ({ const content = { markup: ['html', ...supportedLanguages.markup].includes(config.markup.language) ? config.markup.content - : compiled.markup, + : compiled.markup || '', style: ['css', ...supportedLanguages.style].includes(config.style.language) ? config.style.content - : compiled.style, + : compiled.style || '', script: config.script.language === 'php' ? config.script.content?.replace(/<\?php/g, '') || '' : config.script.language === 'python' ? config.script.content : replaceImports( - (isScriptSupported ? config.script.content : compiled.script) || '', + (isScriptSupported ? config.script.content : compiled.script || '') || '', config, ), + files: config.files.map((file) => ({ + ...file, + content: replaceImports(file.content, config), + })), }; const scriptType = getLanguageCompiler?.(config.script.language)?.scriptType; @@ -171,5 +188,5 @@ ${escapeScript(content.script || '')} return ''; } } - return content[editorId] || ''; + return getSource(editorId, content as any)?.content || ''; }; diff --git a/src/livecodes/handlers/keyboard-shortcuts.ts b/src/livecodes/handlers/keyboard-shortcuts.ts index 4791bcba1e..516e7b6ea5 100644 --- a/src/livecodes/handlers/keyboard-shortcuts.ts +++ b/src/livecodes/handlers/keyboard-shortcuts.ts @@ -1,5 +1,6 @@ import type { createSplitPanes } from '../UI'; import * as UI from '../UI/selectors'; +import { getSource } from '../config/utils'; import type { CodeEditor, Config, EditorId, EventsManager, ToolsPane } from '../models'; import { ctrl } from '../utils'; @@ -8,7 +9,7 @@ import { ctrl } from '../utils'; */ export interface KeyboardShortcutDeps { eventsManager: EventsManager; - getActiveEditor: () => CodeEditor; + getActiveEditor: () => CodeEditor | undefined; getConfig: () => Config; showEditor: (editorId: EditorId) => void; run: () => Promise; @@ -27,7 +28,7 @@ let lastkeys = ''; */ const createCommandPaletteHandler = (deps: KeyboardShortcutDeps) => (e: KeyboardEvent) => { const activeEditor = deps.getActiveEditor(); - if (ctrl(e) && e.code === 'KeyP' && activeEditor.monaco) { + if (ctrl(e) && e.code === 'KeyP' && activeEditor?.monaco) { e.preventDefault(); activeEditor.monaco.trigger('anyString', 'editor.action.quickCommand'); lastkeys = 'Ctrl + P'; @@ -108,7 +109,7 @@ const createZoomToggleHandler = () => (e: KeyboardEvent) => { const createFocusEditorHandler = (deps: KeyboardShortcutDeps) => (e: KeyboardEvent) => { if (ctrl(e) && e.altKey && e.code === 'KeyE') { e.preventDefault(); - deps.getActiveEditor().focus(); + deps.getActiveEditor()?.focus(); lastkeys = 'Ctrl + Alt + E'; return true; } @@ -149,25 +150,26 @@ const createEscapeHandler = (deps: KeyboardShortcutDeps) => (e: KeyboardEvent) = // Ctrl + Alt + (1-3) activates editor 1-3 // Ctrl + Alt + (ArrowLeft/ArrowRight) activates previous/next editor const createEditorSwitchHandler = (deps: KeyboardShortcutDeps) => (e: KeyboardEvent) => { - if (ctrl(e) && e.altKey && ['1', '2', '3', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { - const editorIds = (['markup', 'style', 'script'] as const).filter( - (id) => deps.getConfig()[id].hideTitle !== true, - ); - + const config = deps.getConfig(); + const editorIds = ( + config.files.length + ? config.files.map((f) => f.filename) + : (['markup', 'style', 'script'] as EditorId[]) + ).filter((id) => getSource(id, config)?.hidden !== true); + const editorNumbers = editorIds.map((_, id) => String(id + 1)); + + if (ctrl(e) && e.altKey && [...editorNumbers, 'ArrowLeft', 'ArrowRight'].includes(e.key)) { e.preventDefault(); deps.split?.show('code'); - - const index = ['1', '2', '3'].includes(e.key) + const index = editorNumbers.includes(e.key) ? Number(e.key) - 1 : e.key === 'ArrowLeft' - ? editorIds.findIndex((id) => id === deps.getConfig().activeEditor) - 1 || 0 + ? editorIds.findIndex((id) => id === config.activeEditor) - 1 || 0 : e.key === 'ArrowRight' - ? editorIds.findIndex((id) => id === deps.getConfig().activeEditor) + 1 || 0 + ? editorIds.findIndex((id) => id === config.activeEditor) + 1 || 0 : 0; - const editorIndex = index === editorIds.length ? 0 : index === -1 ? editorIds.length - 1 : index; - deps.showEditor(editorIds[editorIndex]); lastkeys = 'Ctrl + Alt + ' + e.key; return true; diff --git a/src/livecodes/html/app.html b/src/livecodes/html/app.html index a5764a655b..067d88e71a 100644 --- a/src/livecodes/html/app.html +++ b/src/livecodes/html/app.html @@ -132,7 +132,15 @@
-
+
+ +
+ +
diff --git a/src/livecodes/html/import.html b/src/livecodes/html/import.html index 67e831e0d5..7c9dbfe981 100644 --- a/src/livecodes/html/import.html +++ b/src/livecodes/html/import.html @@ -56,7 +56,7 @@
  • Code in web page DOM
  • Code in zip file
  • Code in image (OCR)
  • -
  • Official playgrounds
    (TypeScript and Vue)
  • +
  • Official playgrounds
    (TypeScript, Vue, Svelte, Preact and Solid)
  • Please visit the +
  • + + Multi-file Templates + +
  • My Templates
  • @@ -34,6 +39,15 @@
    +
    + +