diff --git a/README.md b/README.md index 25fe1db7e2..9ed3c8814c 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Add this code to your page: - [Client-side!](https://livecodes.io/docs/why#client-side) - Very [configurable](https://livecodes.io/docs/configuration/) - Developer-friendly build-free environment -- Powerful [SDK](https://livecodes.io/docs/sdk/) (available for [vanilla JavaScript, TypeScript](https://livecodes.io/docs/sdk/js-ts), [React](https://livecodes.io/docs/sdk/react), [Vue](https://livecodes.io/docs/sdk/vue), [Svelte](https://livecodes.io/docs/sdk/svelte) and [Solid](https://livecodes.io/docs/sdk/solid)) +- Powerful [SDK](https://livecodes.io/docs/sdk/) (available for [vanilla JavaScript, TypeScript](https://livecodes.io/docs/sdk/js-ts), [React](https://livecodes.io/docs/sdk/react), [Vue](https://livecodes.io/docs/sdk/vue), [Svelte](https://livecodes.io/docs/sdk/svelte), [Solid](https://livecodes.io/docs/sdk/solid) and [Ripple](https://livecodes.io/docs/sdk/ripple)) - Comprehensive [Documentations](https://livecodes.io/docs/) - Focused on [privacy and security](https://livecodes.io/docs/features/security) - Free and [Open-Source](https://livecodes.io/docs/license) @@ -156,7 +156,7 @@ createPlayground('#container', { }); ``` -The [JavaScript SDK](https://livecodes.io/docs/sdk/js-ts) is framework/library agnostic. However, wrapper components are also provided for popular libraries (currently [React](https://livecodes.io/docs/sdk/react) and [Vue](https://livecodes.io/docs/sdk/vue)). The SDK can be used in [Svelte](https://livecodes.io/docs/sdk/svelte) and [Solid](https://livecodes.io/docs/sdk/solid) directly without wrappers. [TypeScript support](https://livecodes.io/docs/sdk/js-ts#typescript-types) provides type-safety and a great developer experience. +The [JavaScript SDK](https://livecodes.io/docs/sdk/js-ts) is framework/library agnostic. However, wrapper components are also provided for popular libraries (currently [React](https://livecodes.io/docs/sdk/react) and [Vue](https://livecodes.io/docs/sdk/vue)). The SDK can be used in [Svelte](https://livecodes.io/docs/sdk/svelte), [Solid](https://livecodes.io/docs/sdk/solid) and [Ripple](https://livecodes.io/docs/sdk/ripple) directly without wrappers. [TypeScript support](https://livecodes.io/docs/sdk/js-ts#typescript-types) provides type-safety and a great developer experience. React SDK example: ([open in LiveCodes](https://livecodes.io/?x=code/N4IgLglmA2CmIC4QBkIDdYGED2ATWAzgAQBKsAhgMZhEDKAIgNJH4C22IANCPgZQE4QADpGwA7RCC4gAFhVySAPK1hhyRSjPL8CqgLwAdEAFUAKgDEAtAA4jRAPQA+A2OWr1Y8isMg0EWADuQtj8YHaU4mCwYmA+ARC4YDJ6+H6UsJbxiTKcRBBiUBDk0JZ8xbB6AIwAdAAMdk7SMmCs0ACCYGA6ktDkYgDmPtHhvQQEPkbSav0EiADaALrcVJAYAKK4UCGSfIIi0qzaANYArkKIoL0DJ+T98EjNrdIRMdFgkiAAvtwEYACecAuICu-Rud0klDGz0ibw+3xAu2E7wQlz6oNu9xAACsCAAPaGvGKSCCsYKhIioDA4XhEABm-GwrCIAHJoOhYBFePZ+BRqMyANwGfguIUuF6-DTiWkQfpEPREYCisREIiHfinIQIBVKlUqkFg2Ba5lqo64bABMTMzg63UvKIxI0AYiIAAlYNBoNgiAB1ELQXAAQitNu+Ss+guFYnFNAACr0-v0GScxLg5UQABQASjljiIikpWDwhElYmlg2ALzLnyIfkCPh5BBO0DCIAcjgjLlguLJNHwtPITdj8cT2GTuA7EnhvwBhDkqlmCEWPwESIXS5AkIIMYbqg+3CEDPSYxCa6WG5Ov0ZtFUkAGC+A8JJPfv8P+QkIF1fhDA9+BaINkhvoQK77NwdqwkgXzcBgOgQOIkgAMwIVM2DYNAv7ROQABGcAKEgxTQNIKzshC4gEGh8A-GoYAXhCnq6AonyfEAA)) diff --git a/docs/docs/languages/ripple.mdx b/docs/docs/languages/ripple.mdx new file mode 100644 index 0000000000..0d20e38610 --- /dev/null +++ b/docs/docs/languages/ripple.mdx @@ -0,0 +1,439 @@ +--- +toc_max_heading_level: 4 +--- + +# Ripple + +[Ripple](https://www.ripplejs.com/) is a TypeScript UI framework that takes the best parts of React, Solid and Svelte and combines them into one package. + +Ripple components can be used in LiveCodes as documented in the [Ripple docs](https://github.com/trueadm/ripple). See below for usage. + +## Demo + +import LiveCodes from '../../src/components/LiveCodes.tsx'; +import RunInLiveCodes from '../../src/components/RunInLiveCodes.tsx'; + +export const demo = { + activeEditor: 'script', + script: { + language: 'ripple', + content: `import { track } from 'ripple'; + +export default component Counter() { + let count = track(0); + let doubled = track(() => @count * 2); + +
+

{'Counter'}

+

{\`Count: \${@count}\`}

+

{\`Doubled: \${@doubled}\`}

+ + + +
+ + +}`, + }, +} + + + + +## Mounting + +### Auto-rendering + +A component is mounted and rendered automatically as a Ripple component (without having to [manually mount](#manual-mount) it) if the following conditions are met: + +- The component is exported as the default export. +- No [imports from `"./script"`](#exports) in markup editor. +- Auto-rendering is not [disabled](#disabling-auto-rendering). + +### Root Element + +To mount the application instance to a specific DOM element use `"livecodes-app"` as the element `id` in the HTML. Otherwise, if that element is not found, a new `div` element is added to `document.body` and is used to mount the instance. +In case you need more control, you can [manually mount](#manual-mount) the instance. + +Example: + +export const customRoot = { + markup: { + language: 'html', + content: `

Custom Root Element

+
+
...other page content
+`, + }, + script: { + language: 'ripple', + content: `import { track } from 'ripple'; + +export default component App() { + let name = track('Ripple'); +
{\`I'm a \${@name} component\`}
+ +} +`, + }, +}; + + + + +### Manual Mount + +Exports from Ripple code can be imported in [markup editor](../features/projects.mdx#markup-editor) from `"./Component.ripple"` and used to mount the application instance manually. +See [Exports](#exports) for details. + +Example: + +export const manualMount = { + markup: { + language: 'html', + content: `

Manual Mount

+
+
...other page content
+ + +`, + }, + script: { + language: 'ripple', + content: `export component App(props: { name: string }) { +
{\`I'm a \${props.name} component\`}
+ +} +`, + }, +}; + + + +### Disabling Auto-rendering + +To disable [auto-rendering](#auto-rendering), set the [custom settings](#custom-settings) `disableAutoRender` property to `true`. + +```json title="Custom Settings" +{ + "ripple": { + "disableAutoRender": true + } +} +``` + +## Importing Modules + +### NPM Modules + +npm modules can be imported as described in the section about [module resolution](../features/module-resolution.mdx), including bare module imports and importing from different CDNs. Stylesheets imported in the `script` block are added as `` tags in the page `head` (see [below](#importing-stylesheets)). + +Module imports can be customized using import maps as described in [module resolution](../features/module-resolution.mdx#custom-module-resolution) documentations. + +Example: + +export const importsDemo = { + ripple: `import { track, effect } from "ripple"; +import confetti from "canvas-confetti"; + +export default component App() { +
+ let count = track(0); + + effect(() => { + if (@count === 2) confetti(); + }); + + + + if (@count > 1) { +
{'Greater than 1!'}
+ } +
+ + +} +`, +}; + + + +### External Components + +External Ripple components can be imported. +The import URL can be either: +- An absolute URL ending with `.ripple` extension. +- A [bare module import](../features/module-resolution.mdx#bare-module-imports) for a published npm module with full path to the component with extension. +- A [data URL](../features/data-urls.mdx) starting with `data:text/ripple`. + +Any bare or relative imports in the imported files are resolved and compiled recursively. +If a processor is enabled (e.g. Tailwind CSS), classes in the imported files are detected and used in the generated CSS (see [CSS Frameworks](#css-frameworks) section below). + +Example: ([source](https://github.com/hatemhosny/ripple-tailwind-counter-demo)) + +export const externalComponents = { + activeEditor: 'script', + script: { + language: 'ripple', + content: `// import from npm modules +import { track } from 'ripple'; +import { Counter } from 'ripple-tailwind-counter-demo/src/Counter.ripple'; +// alternatively use absolute URLs +// import { Counter } from 'https://raw.githubusercontent.com/hatemhosny/ripple-tailwind-counter-demo/refs/heads/main/src/Counter.ripple'; + +export default component App() { + let name = track("World"); + + setTimeout(() => { + @name = "Ripple + Tailwind!"; + }, 2000); + + +} +`, + }, + processors: ['tailwindcss'], +} + + + +### Exports + +Values exported from [script editor](../features/projects.mdx#script-editor) (default or named) can be imported in the [markup editor](../features/projects.mdx#markup-editor) by importing from `"./script"` (with no extension). + +This can be useful, for example, for [manually mounting](#manual-mount) components in the markup editor. + +:::info note + +When values are imported from `"./script"`, [auto-rendering](#auto-rendering) is disabled, because it is assumed that you want to take control over component rendering. + +::: + +## Styles + +CSS can be applied to the component using various ways: + +### Component Styles + +Styles in a `style` tag in a Ripple component are applied to that component only. + +CSS processors (e.g. SCSS, Stylus) can be used by specifying a `lang` attribute for the `style` tag. + +Example: + +export const styleTag = { + ripple: `export default component App() { +

{'Hello World!'}

+ + +} +` +} + + + + +### Style Editor + +Styles added in the [style editor](../features/projects.mdx#style-editor) are applied globally to the [result page](../features/result.mdx). This can use different **languages/processors** supported in LiveCodes including CSS, SCSS, Less, Stylus, ..etc. See [style documentation](../features/css.mdx) for more details. + +And of course, styles and stylesheets added in [markup editor](../features/projects.mdx#markup-editor) are also applied globally. + +### Importing Stylesheets + +Stylesheets imported in [script editor](../features/projects.mdx#script-editor) are added as `` tags in the page `head`. +The stylesheet URL can be an absolute URL or a path in the npm package. The URL has to end with `".css"`. + +example: + +export const stylesDemo = { + ripple: `import "bootstrap/dist/css/bootstrap.css";\n\nexport default component App() {\n

{'Hello World!'}

\n}\n`, +}; + + + +### CSS Modules + +CSS modules are supported and are [documented separately](./cssmodules.mdx). Make sure to enable CSS modules (from style editor menu or in [`processors`](../configuration/configuration-object.mdx#processors) property of [configuration object](../configuration/configuration-object.mdx)). + +Demo: + +export const cssModulesDemo = { + activeEditor: 'script', + style: { language: 'css', content: `.title {\n color: green;\n font-family: sans-serif;\n}\n` }, + script: { + language: 'ripple', + content: `import classes from './style.module.css';\n\nexport default component() {\n

\n {'Hello, CSS Modules!'}\n

\n}\n`, + }, + processors: ['cssmodules'], +}; + + + +### CSS Frameworks + +[CSS Frameworks](../features/css.mdx#css-processors) supported in LiveCodes (e.g. [Tailwind CSS](./tailwindcss.mdx), [UnoCSS](./unocss.mdx), [WindiCSS](./windicss.mdx)) can detect class names added in Ripple components. +Make sure that the required utility is enabled (from style editor menu or in `processors` property of [configuration object](../configuration/configuration-object.mdx#processors)). + +Example: + +export const tailwindDemo = { + activeEditor: 'script', + script: { + language: 'ripple', + content: `import { track } from 'ripple'; + +export default component Counter() { + let count = track(0); + +
+

{'Ripple + Tailwind CSS'}

+
+ + {@count} + +
+ +
+ + +} +`, + }, + processors: ['tailwindcss'], +}; + + + +## Language Info + +### Name + +`ripple` + +### Extensions + +`.ripple` + +### Editor + +`script` + +## Compiler + +The official [Ripple compiler](https://github.com/trueadm/ripple). + +### Version + +`ripple`: v0.2.15 + +## Code Formatting + +Using [Prettier](https://prettier.io/). + +## Custom Settings + +[Custom settings](../advanced/custom-settings.mdx) can be added to the property `ripple`. +These include: + +- `"disableAutoRender"` - [disables auto-rendering](#disabling-auto-rendering) (default: `false`). +- `"version"` - specifies the version of Ripple compiler (default: `"latest"`). + +Example: + +```json title="Custom Settings" +{ + "ripple": { + "version": "0.2.15" + } +} +``` + +Please note that custom settings should be valid JSON (i.e. functions are not allowed). + +## Starter Template + +https://livecodes.io/?template=ripple + + + +## Links + +- [Ripple](https://ripplejs.com/) +- [Ripple documentations](https://github.com/trueadm/ripple) diff --git a/docs/docs/sdk/index.mdx b/docs/docs/sdk/index.mdx index db5b604aef..e1e6a53b36 100644 --- a/docs/docs/sdk/index.mdx +++ b/docs/docs/sdk/index.mdx @@ -10,7 +10,7 @@ The Software Development Kit (SDK) provides an easy, yet powerful, interface to The SDK is provided as a light-weight ([less than 5kb gzipped](https://bundlephobia.com/package/livecodes)), zero-dependencies [npm package](#npm-package), that is also available from [CDNs](#cdn). It can be used to create playgrounds with a wide variety of [configurations](../configuration/configuration-object.mdx) and [embed options](js-ts.mdx#embed-options). In addition, [SDK methods](js-ts.mdx#sdk-methods) allow programmatic communication and control of the playgrounds during runtime. -The [JavaScript SDK](js-ts.mdx) is framework/library agnostic. However, wrapper components are also provided for popular libraries (currently [React](react.mdx) and [Vue](vue.mdx)). The SDK can be used in [Svelte](svelte.mdx) and [Solid](solid.mdx) directly without wrappers. [TypeScript support](js-ts.mdx#typescript-types) provides type-safety and a great developer experience. +The [JavaScript SDK](js-ts.mdx) is framework/library agnostic. However, wrapper components are also provided for popular libraries (currently [React](react.mdx) and [Vue](vue.mdx)). The SDK can be used in [Svelte](svelte.mdx), [Solid](solid.mdx) and [Ripple](ripple.mdx) directly without wrappers. [TypeScript support](js-ts.mdx#typescript-types) provides type-safety and a great developer experience. ## SDK Demo @@ -99,14 +99,11 @@ In the full [standalone app](../getting-started.mdx#standalone-app), the JavaScr The SDK is currently provided in the following variations: - [JavaScript/TypeScript](./js-ts.mdx) - - [React](./react.mdx) - - [Vue](./vue.mdx) - - [Svelte](./svelte.mdx) - - [Solid](./solid.mdx) +- [Ripple](./ripple.mdx) ## Headless Mode diff --git a/docs/docs/sdk/js-ts.mdx b/docs/docs/sdk/js-ts.mdx index 1eb4ef14f4..4c12e7b659 100644 --- a/docs/docs/sdk/js-ts.mdx +++ b/docs/docs/sdk/js-ts.mdx @@ -602,4 +602,5 @@ export const sdkDemo = { - [React SDK](./react.mdx) - [Vue SDK](./vue.mdx) - [Using SDK in Svelte](./svelte.mdx) +- [Using SDK in Ripple](./ripple.mdx) - [Embedded Playgrounds](../features/embeds.mdx) diff --git a/docs/docs/sdk/react.mdx b/docs/docs/sdk/react.mdx index ef8177c3ea..1c54af2d5d 100644 --- a/docs/docs/sdk/react.mdx +++ b/docs/docs/sdk/react.mdx @@ -149,4 +149,5 @@ export const reactSDKDemo = { - [Vue SDK](./vue.mdx) - [Using SDK in Svelte](./svelte.mdx) - [Using SDK in Solid](./solid.mdx) +- [Using SDK in Ripple](./ripple.mdx) - [Embedded Playgrounds](../features/embeds.mdx) diff --git a/docs/docs/sdk/ripple.mdx b/docs/docs/sdk/ripple.mdx new file mode 100644 index 0000000000..cb42b14e5c --- /dev/null +++ b/docs/docs/sdk/ripple.mdx @@ -0,0 +1,82 @@ +# Ripple + +import LiveCodes from '../../src/components/LiveCodes.tsx'; +import RunInLiveCodes from '../../src/components/RunInLiveCodes.tsx'; + +The [JS/TS SDK](js-ts.mdx) can be used directly in [Ripple](https://ripplejs.com) components without the need for any wrappers. + +## Installation + +Please refer to the [SDK installation](./index.mdx#installation) section. + +## Usage + +This is an example of using the LiveCodes JS SDK in a Ripple component: + +```jsx title="App.ripple" +import { createPlayground, type EmbedOptions, type Playground } from 'livecodes'; + +export default component App() { + const options: EmbedOptions = { + params: { + html: '

Hello World!

', + css: 'h1 {color: blue;}', + js: 'console.log("Hello, Ripple!")', + console: 'open', + }, + }; + + let playground: Playground | null = null; + const onMount = (container: HTMLElement) => { + createPlayground(container, options).then((sdk) => { + playground = sdk; // now the SDK is available + }); + return () => playground?.destroy(); + }; + +
+} +``` + +export const rippleSDKDemo = { + ripple: `import { createPlayground, type EmbedOptions, type Playground } from 'livecodes'; + +export default component App() { + const options: EmbedOptions = { + params: { + html: '

Hello World!

', + css: 'h1 {color: blue;}', + js: 'console.log("Hello, Ripple!")', + console: 'open', + }, + }; + + let playground: Playground | null = null; + const onMount = (container: HTMLElement) => { + createPlayground(container, options).then((sdk) => { + playground = sdk; // now the SDK is available + }); + return () => playground?.destroy(); + }; + +
+} +`, +}; + + + +[Embed options](./js-ts.mdx#embed-options), [SDK methods](./js-ts.mdx#sdk-methods) and [TypeScript types](./js-ts.mdx#typescript-types) are available as described in the [JS/TS SDK documentations](./js-ts.mdx). + +## Demo + + + +## Related + +- [SDK Installation](./index.mdx#installation) +- [JS/TS SDK](./js-ts.mdx) +- [React SDK](./react.mdx) +- [Vue SDK](./vue.mdx) +- [Using SDK in Svelte](./svelte.mdx) +- [Embedded Playgrounds](../features/embeds.mdx) diff --git a/docs/docs/sdk/svelte.mdx b/docs/docs/sdk/svelte.mdx index ff903030b9..43362a5709 100644 --- a/docs/docs/sdk/svelte.mdx +++ b/docs/docs/sdk/svelte.mdx @@ -98,4 +98,5 @@ However, it is recommended to cleanup when the node is unmounted, like that: - [React SDK](./react.mdx) - [Vue SDK](./vue.mdx) - [Using SDK in Solid](./solid.mdx) +- [Using SDK in Ripple](./ripple.mdx) - [Embedded Playgrounds](../features/embeds.mdx) diff --git a/docs/docs/sdk/vue.mdx b/docs/docs/sdk/vue.mdx index 9a2a572a27..a8f9133d05 100644 --- a/docs/docs/sdk/vue.mdx +++ b/docs/docs/sdk/vue.mdx @@ -164,4 +164,5 @@ export const vueSDKDemo = { - [React SDK](./react.mdx) - [Using SDK in Svelte](./svelte.mdx) - [Using SDK in Solid](./solid.mdx) +- [Using SDK in Ripple](./ripple.mdx) - [Embedded Playgrounds](../features/embeds.mdx) diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 815f2cddfb..9acc45ced2 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -290,6 +290,7 @@ const config: Config = { plugin: ['typedoc-plugin-missing-exports'], excludeExternals: true, internalModule: '_internal', + skipErrorChecking: true, }, ], [ diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 1b147ba905..7c39ba1d65 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -85,7 +85,15 @@ const sidebars: SidebarsConfig = { type: 'doc', id: 'sdk/index', }, - items: ['sdk/js-ts', 'sdk/react', 'sdk/vue', 'sdk/svelte', 'sdk/solid', 'sdk/headless'], + items: [ + 'sdk/js-ts', + 'sdk/react', + 'sdk/vue', + 'sdk/svelte', + 'sdk/solid', + 'sdk/ripple', + 'sdk/headless', + ], }, { type: 'category', diff --git a/docs/src/components/HomepageFeatures.tsx b/docs/src/components/HomepageFeatures.tsx index 000ff383af..eda2f7a788 100644 --- a/docs/src/components/HomepageFeatures.tsx +++ b/docs/src/components/HomepageFeatures.tsx @@ -152,8 +152,9 @@ const FeatureList3: FeatureItem[] = [ playgrounds and allows easy communication with them. The SDK is available for vanilla JS/TS,{' '} React, Vue,{' '} - Svelte and Solid. There is also - a headless mode for full control over the UI. + Svelte, Solid and{' '} + Ripple. There is also a{' '} + headless mode for full control over the UI. ), }, diff --git a/docs/src/components/LanguageSliders.tsx b/docs/src/components/LanguageSliders.tsx index 960c4933a6..e7ec272e72 100644 --- a/docs/src/components/LanguageSliders.tsx +++ b/docs/src/components/LanguageSliders.tsx @@ -69,6 +69,7 @@ export default function Sliders() { { name: 'solid.tsx', title: 'Solid (TS)' }, { name: 'riot', title: 'Riot.js' }, { name: 'malina', title: 'Malina.js' }, + { name: 'ripple', title: 'Ripple' }, { name: 'coffeescript', title: 'CoffeeScript' }, { name: 'livescript', title: 'LiveScript' }, { name: 'civet', title: 'Civet' }, diff --git a/docs/src/components/LiveCodes.tsx b/docs/src/components/LiveCodes.tsx index 1c9d1ee716..350192fe00 100644 --- a/docs/src/components/LiveCodes.tsx +++ b/docs/src/components/LiveCodes.tsx @@ -85,6 +85,20 @@ export default function App() { return
; } +`.trimStart(); + + const rippleCode = ` +import { createPlayground, type EmbedOptions } from 'livecodes'; + +export default component App() { + const options: EmbedOptions = ${stringify(options).split('\n').join('\n ')}; + const onMount = (container: HTMLElement) => { + createPlayground(container, options); + }; + +
+} + `.trimStart(); return ( @@ -111,6 +125,7 @@ export default function App() { vue={vueCode} svelte={svelteCode} solid={solidCode} + ripple={rippleCode} > )} diff --git a/docs/src/components/ShowCode.tsx b/docs/src/components/ShowCode.tsx index 20770a74b6..94df3fd368 100644 --- a/docs/src/components/ShowCode.tsx +++ b/docs/src/components/ShowCode.tsx @@ -15,6 +15,7 @@ export default function ShowCode(props: { vue: string; svelte: string; solid: string; + ripple: string; }): ReactNode { const [jsCode, setJsCode] = useState(props.js); const [tsCode, setTsCode] = useState(props.ts); @@ -22,6 +23,7 @@ export default function ShowCode(props: { const [vueCode, setVueCode] = useState(props.vue); const [svelteCode, setSvelteCode] = useState(props.svelte); const [solidCode, setSolidCode] = useState(props.solid); + const [rippleCode, setRippleCode] = useState(props.ripple); const codeBlockTitleHeight = '3.7rem'; const [codeCollapsed, setCodeCollapsed] = useState(true); @@ -60,6 +62,7 @@ export default function ShowCode(props: { setVueCode(format(vueCode, 'html')); setSvelteCode(format(svelteCode, 'html')); setSolidCode(format(solidCode, 'tsx')); + setRippleCode(format(rippleCode, 'tsx')); } }, []); @@ -103,6 +106,9 @@ export default function ShowCode(props: { {solidCode} + + {rippleCode} + diff --git a/docs/src/components/TemplateList.tsx b/docs/src/components/TemplateList.tsx index a246d4d47a..25d77c5114 100644 --- a/docs/src/components/TemplateList.tsx +++ b/docs/src/components/TemplateList.tsx @@ -23,6 +23,7 @@ const templates = [ { name: 'astro', title: 'Astro Starter', thumbnail: 'astro.svg' }, { name: 'riot', title: 'Riot.js Starter', thumbnail: 'riot.svg' }, { name: 'malina', title: 'Malina.js Starter', thumbnail: 'malina.svg' }, + { name: 'ripple', title: 'Ripple Starter', thumbnail: 'ripple-0.png' }, { name: 'jquery', title: 'jQuery Starter', thumbnail: 'jquery.svg' }, { name: 'backbone', title: 'Backbone Starter', thumbnail: 'backbone.svg' }, { name: 'knockout', title: 'Knockout Starter', thumbnail: 'knockout.svg' }, diff --git a/eslint.config.mjs b/eslint.config.mjs index aab8c872fb..bdb275ecf3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -32,6 +32,7 @@ export default [ '**/.jest', '**/.storybook', 'functions/vendors', + 'src/livecodes/editor/monaco/languages/monaco-lang-ripple.ts', ], }, ...fixupConfigRules( diff --git a/scripts/build.js b/scripts/build.js index 60b1976642..17e4f13c3c 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -160,6 +160,7 @@ const esmBuild = () => 'editor/monaco/languages/monaco-lang-astro.ts', 'editor/monaco/languages/monaco-lang-clio.ts', 'editor/monaco/languages/monaco-lang-imba.ts', + 'editor/monaco/languages/monaco-lang-ripple.ts', // 'editor/monaco/languages/monaco-lang-sql.ts', 'editor/monaco/languages/monaco-lang-wat.ts', 'editor/codemirror/codemirror.ts', @@ -237,6 +238,8 @@ const iifeBuild = () => 'languages/python-wasm/lang-python-wasm-script.ts', 'languages/rescript/lang-rescript-formatter.ts', 'languages/riot/lang-riot-compiler.ts', + 'languages/ripple/lang-ripple-compiler.ts', + 'languages/ripple/lang-ripple-formatter.ts', 'languages/ruby-wasm/lang-ruby-wasm-script.ts', 'languages/scss/lang-scss-compiler.ts', 'languages/solid/lang-solid-compiler.ts', diff --git a/src/livecodes/UI/command-menu-actions.ts b/src/livecodes/UI/command-menu-actions.ts index 708f5ba1f7..be3be12b67 100644 --- a/src/livecodes/UI/command-menu-actions.ts +++ b/src/livecodes/UI/command-menu-actions.ts @@ -266,6 +266,7 @@ export const getCommandMenuActions = ({ 'preact', 'svelte', 'solid', + 'ripple', 'lit', 'stencil', 'mdx', diff --git a/src/livecodes/assets/templates/ripple.png b/src/livecodes/assets/templates/ripple.png new file mode 100644 index 0000000000..9a58b221f2 Binary files /dev/null and b/src/livecodes/assets/templates/ripple.png differ diff --git a/src/livecodes/compiler/__tests__/compile-blocks.spec.ts b/src/livecodes/compiler/__tests__/compile-blocks.spec.ts new file mode 100644 index 0000000000..edf88604c5 --- /dev/null +++ b/src/livecodes/compiler/__tests__/compile-blocks.spec.ts @@ -0,0 +1,60 @@ +import type { Config } from '../../models'; +import { compileBlocks, maskComments } from '../compile-blocks'; + +describe('compileBlocks', () => { + const config = { processors: [] } as unknown as Config; + + test('keeps several unprocessed blocks in document order', async () => { + const code = ` +export function App() @{ + <> + +
Hello
+ + +} + +function Child() @{ + <> + +

Notice

+ +} +`; + expect(await compileBlocks(code, 'style', config)).toBe(code); + }); + + test('does not take a tag inside a comment for a block', async () => { + const code = ` +// A ; +export function App() @{ + <> + +
Hello
+ +} +`; + expect(await compileBlocks(code, 'style', config, { ignoreComments: true })).toBe(code); + }); + + test('keeps a single block untouched', async () => { + const code = `\n\n`; + expect(await compileBlocks(code, 'style', config)).toBe(code); + }); +}); + +describe('maskComments', () => { + test('blanks line and block comments, keeping indexes and line breaks', () => { + const code = 'a // one\nb /* two\nthree */ c'; + const masked = maskComments(code); + expect(masked).toHaveLength(code.length); + expect(masked).toBe('a \nb \n c'); + }); + + test('leaves strings, template literals, and URLs alone', () => { + const code = "const a = 'http://x // y'; const b = `//${1}`; url(http://z) )', + contentName: 'source.css', + patterns: [ + { + include: 'source.css', + }, + ], + }, + { + begin: '(?<=]*/>)', + end: '(?=>)', + patterns: [ + { + include: '#jsx-tag-attributes', + }, + ], + }, + ], + }, + 'jsx-tag': { + name: 'meta.tag.js', + begin: + '(?=(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?))', + end: '(/>)|(?:())', + endCaptures: { + '1': { + name: 'punctuation.definition.tag.end.js', + }, + '2': { + name: 'punctuation.definition.tag.begin.js', + }, + '3': { + name: 'entity.name.tag.namespace.js', + }, + '4': { + name: 'punctuation.separator.namespace.js', + }, + '5': { + name: 'entity.name.tag.js', + }, + '6': { + name: 'support.class.component.js', + }, + '7': { + name: 'punctuation.definition.tag.end.js', + }, + }, + patterns: [ + { + begin: + '(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?)', + beginCaptures: { + '1': { + name: 'punctuation.definition.tag.begin.js', + }, + '2': { + name: 'entity.name.tag.namespace.js', + }, + '3': { + name: 'punctuation.separator.namespace.js', + }, + '4': { + name: 'entity.name.tag.js', + }, + '5': { + name: 'support.class.component.js', + }, + }, + end: '(?=[/]?>)', + patterns: [ + { + include: '#comment', + }, + { + include: '#type-arguments', + }, + { + include: '#jsx-tag-attributes', + }, + ], + }, + { + begin: '(>)', + beginCaptures: { + '1': { + name: 'punctuation.definition.tag.end.js', + }, + }, + end: '(?=)', + patterns: [ + { + include: '#comment', + }, + { + include: '#jsx-tag-attribute-name', + }, + { + include: '#jsx-tag-attribute-assignment', + }, + { + include: '#jsx-string-double-quoted', + }, + { + include: '#jsx-string-single-quoted', + }, + { + include: '#jsx-evaluated-code', + }, + { + include: '#jsx-tag-attributes-illegal', + }, + ], + }, + 'jsx-tag-attribute-name': { + match: + '(?x)\n \\s*\n (?:([_$[:alpha:]][-_$[:alnum:].]*)(:))?\n ([_$[:alpha:]][-_$[:alnum:]]*)\n (?=\\s|=|/?>|/\\*|//)', + captures: { + '1': { + name: 'entity.other.attribute-name.namespace.js', + }, + '2': { + name: 'punctuation.separator.namespace.js', + }, + '3': { + name: 'entity.other.attribute-name.js', + }, + }, + }, + 'jsx-tag-attribute-assignment': { + name: 'keyword.operator.assignment.js', + match: '=(?=\\s*(?:\'|"|{|/\\*|//|\\n))', + }, + 'jsx-string-double-quoted': { + name: 'string.quoted.double.js', + begin: '"', + end: '"', + beginCaptures: { + '0': { + name: 'punctuation.definition.string.begin.js', + }, + }, + endCaptures: { + '0': { + name: 'punctuation.definition.string.end.js', + }, + }, + patterns: [ + { + include: '#jsx-entities', + }, + ], + }, + 'jsx-string-single-quoted': { + name: 'string.quoted.single.js', + begin: "'", + end: "'", + beginCaptures: { + '0': { + name: 'punctuation.definition.string.begin.js', + }, + }, + endCaptures: { + '0': { + name: 'punctuation.definition.string.end.js', + }, + }, + patterns: [ + { + include: '#jsx-entities', + }, + ], + }, + 'server-block': { + name: 'meta.server-block.js', + begin: '(#server)\\s*(\\{)', + beginCaptures: { + '1': { + name: 'storage.type.server.js', + }, + '2': { + name: 'punctuation.definition.block.js', + }, + }, + end: '\\}', + endCaptures: { + '0': { + name: 'punctuation.definition.block.js', + }, + }, + patterns: [ + { + include: '#statements', + }, + ], + }, + 'server-member-expression': { + name: 'meta.server-member-expression.js', + match: '(#server)(\\.)([_$[:alpha:]][_$[:alnum:]]*)', + captures: { + '1': { + name: 'storage.type.server.js', + }, + '2': { + name: 'punctuation.accessor.js', + }, + '3': { + name: 'entity.name.function.js', + }, + }, + }, + 'jsx-ref-modifier': { + name: 'storage.modifier.js', + match: '\\b(ref)(?=\\s+[_$[:alpha:]])', + captures: { + '1': { + name: 'storage.modifier.js', + }, + }, + }, + 'jsx-tag-attributes-illegal': { + name: 'invalid.illegal.attribute.js', + match: '\\S+', + }, + }, + }, + + config: { + // Note that this file should stay in sync with 'typescript-language-basics/language-configuration.json' + // onEnterRules cause errors - currently disabled + comments: { + lineComment: '//', + blockComment: ['/*', '*/'], + }, + brackets: [ + ['${', '}'], + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { + open: '{', + close: '}', + }, + { + open: '[', + close: ']', + }, + { + open: '(', + close: ')', + }, + { + open: "'", + close: "'", + notIn: ['string', 'comment'], + }, + { + open: '"', + close: '"', + notIn: ['string'], + }, + { + open: '`', + close: '`', + notIn: ['string', 'comment'], + }, + { + open: '/**', + close: ' */', + notIn: ['string'], + }, + ], + surroundingPairs: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ["'", "'"], + ['"', '"'], + ['`', '`'], + ['<', '>'], + ], + autoCloseBefore: ';:.,=}])>` \n\t', + folding: { + markers: { + start: '^\\s*//\\s*#?region\\b', + end: '^\\s*//\\s*#?endregion\\b', + }, + }, + wordPattern: { + pattern: + '(-?\\d*\\.\\d\\w*)|([^\\`\\~\\@\\!\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\\'\\"\\,\\.\\<\\>/\\?\\s]+)', + }, + indentationRules: { + decreaseIndentPattern: { + pattern: '^\\s*[\\}\\]\\)].*$', + }, + increaseIndentPattern: { + pattern: '^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$', + }, + // e.g. * ...| or */| or *-----*/| + unIndentedLinePattern: { + pattern: + '^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$|^(\\t|[ ])*[ ]\\*/\\s*$|^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$', + }, + indentNextLinePattern: { + pattern: '^((.*=>\\s*)|((.*[^\\w]+|\\s*)(if|while|for)\\s*\\(.*\\)\\s*))$', + }, + }, + // onEnterRules: [ + // { + // // e.g. /** | */ + // beforeText: { + // pattern: '^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$', + // }, + // afterText: { + // pattern: '^\\s*\\*/$', + // }, + // action: { + // indent: 'indentOutdent', + // appendText: ' * ', + // }, + // }, + // { + // // e.g. /** ...| + // beforeText: { + // pattern: '^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$', + // }, + // action: { + // indent: 'none', + // appendText: ' * ', + // }, + // }, + // { + // // e.g. * ...| + // beforeText: { + // pattern: '^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$', + // }, + // previousLineText: { + // pattern: '(?=^(\\s*(/\\*\\*|\\*)).*)(?=(?!(\\s*\\*/)))', + // }, + // action: { + // indent: 'none', + // appendText: '* ', + // }, + // }, + // { + // // e.g. */| + // beforeText: { + // pattern: '^(\\t|[ ])*[ ]\\*/\\s*$', + // }, + // action: { + // indent: 'none', + // removeText: 1, + // }, + // }, + // { + // // e.g. *-----*/| + // beforeText: { + // pattern: '^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$', + // }, + // action: { + // indent: 'none', + // removeText: 1, + // }, + // }, + // { + // beforeText: { + // pattern: '^\\s*(\\bcase\\s.+:|\\bdefault:)$', + // }, + // afterText: { + // pattern: '^(?!\\s*(\\bcase\\b|\\bdefault\\b))', + // }, + // action: { + // indent: 'indent', + // }, + // }, + // { + // // Decrease indentation after single line if/else if/else, for, or while + // previousLineText: '^\\s*(((else ?)?if|for|while)\\s*\\(.*\\)\\s*|else\\s*)$', + // // But make sure line doesn't have braces or is not another if statement + // beforeText: '^\\s+([^{i\\s]|i(?!f\\b))', + // action: { + // indent: 'outdent', + // }, + // }, + // // Indent when pressing enter from inside () + // { + // beforeText: '^.*\\([^\\)]*$', + // afterText: '^\\s*\\).*$', + // action: { + // indent: 'indentOutdent', + // appendText: '\t', + // }, + // }, + // // Indent when pressing enter from inside {} + // { + // beforeText: '^.*\\{[^\\}]*$', + // afterText: '^\\s*\\}.*$', + // action: { + // indent: 'indentOutdent', + // appendText: '\t', + // }, + // }, + // // Indent when pressing enter from inside [] + // { + // beforeText: '^.*\\[[^\\]]*$', + // afterText: '^\\s*\\].*$', + // action: { + // indent: 'indentOutdent', + // appendText: '\t', + // }, + // }, + // // Add // when pressing enter from inside line comment + // { + // beforeText: { + // pattern: '(?>>', + name: 'invalid.deprecated.combinator.css', + }, + { + match: '>>|>|\\+|~', + name: 'keyword.operator.combinator.css', + }, + ], + }, + commas: { + match: ',', + name: 'punctuation.separator.list.comma.css', + }, + 'comment-block': { + begin: '/\\*', + beginCaptures: { + '0': { + name: 'punctuation.definition.comment.begin.css', + }, + }, + end: '\\*/', + endCaptures: { + '0': { + name: 'punctuation.definition.comment.end.css', + }, + }, + name: 'comment.block.css', + }, + escapes: { + patterns: [ + { + match: '\\\\[0-9a-fA-F]{1,6}', + name: 'constant.character.escape.codepoint.css', + }, + { + begin: '\\\\$\\s*', + end: '^(?<:=]|\\)|/\\*) # Terminates cleanly', + }, + 'media-feature-keywords': { + match: + '(?xi)\n(?<=^|\\s|:|\\*/)\n(?: portrait # Orientation\n | landscape\n | progressive # Scan types\n | interlace\n | fullscreen # Display modes\n | standalone\n | minimal-ui\n | browser\n)\n(?=\\s|\\)|$)', + name: 'support.constant.property-value.css', + }, + 'media-query': { + begin: '\\G', + end: '(?=\\s*[{;])', + patterns: [ + { + include: '#comment-block', + }, + { + include: '#escapes', + }, + { + include: '#media-types', + }, + { + match: '(?i)(?<=\\s|^|,|\\*/)(only|not)(?=\\s|{|/\\*|$)', + name: 'keyword.operator.logical.$1.media.css', + }, + { + match: '(?i)(?<=\\s|^|\\*/|\\))and(?=\\s|/\\*|$)', + name: 'keyword.operator.logical.and.media.css', + }, + { + match: ',(?:(?:\\s*,)+|(?=\\s*[;){]))', + name: 'invalid.illegal.comma.css', + }, + { + include: '#commas', + }, + { + begin: '\\(', + beginCaptures: { + '0': { + name: 'punctuation.definition.parameters.begin.bracket.round.css', + }, + }, + end: '\\)', + endCaptures: { + '0': { + name: 'punctuation.definition.parameters.end.bracket.round.css', + }, + }, + patterns: [ + { + include: '#media-features', + }, + { + include: '#media-feature-keywords', + }, + { + match: ':', + name: 'punctuation.separator.key-value.css', + }, + { + match: '>=|<=|=|<|>', + name: 'keyword.operator.comparison.css', + }, + { + captures: { + '1': { + name: 'constant.numeric.css', + }, + '2': { + name: 'keyword.operator.arithmetic.css', + }, + '3': { + name: 'constant.numeric.css', + }, + }, + match: '(\\d+)\\s*(/)\\s*(\\d+)', + name: 'meta.ratio.css', + }, + { + include: '#numeric-values', + }, + { + include: '#comment-block', + }, + ], + }, + ], + }, + 'media-query-list': { + begin: '(?=\\s*[^{;])', + end: '(?=\\s*[{;])', + patterns: [ + { + include: '#media-query', + }, + ], + }, + 'media-types': { + captures: { + '1': { + name: 'support.constant.media.css', + }, + '2': { + name: 'invalid.deprecated.constant.media.css', + }, + }, + match: + '(?xi)\n(?<=^|\\s|,|\\*/)\n(?:\n # Valid media types\n (all|print|screen|speech)\n |\n # Deprecated in Media Queries 4: http://dev.w3.org/csswg/mediaqueries/#media-types\n (aural|braille|embossed|handheld|projection|tty|tv)\n)\n(?=$|[{,\\s;]|/\\*)', + }, + 'numeric-values': { + patterns: [ + { + captures: { + '1': { + name: 'punctuation.definition.constant.css', + }, + }, + match: '(#)(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\\b', + name: 'constant.other.color.rgb-value.hex.css', + }, + { + captures: { + '1': { + name: 'keyword.other.unit.percentage.css', + }, + '2': { + name: 'keyword.other.unit.${2:/downcase}.css', + }, + }, + match: + '(?xi) (?+~|] # - Followed by another selector\n | /\\* # - Followed by a block comment\n )\n |\n # Name contains unescaped ASCII symbol\n (?: # Check for acceptable preceding characters\n [-a-zA-Z_0-9]|[^\\x00-\\x7F] # - Valid selector character\n | \\\\(?:[0-9a-fA-F]{1,6}|.) # - Escape sequence\n )*\n (?: # Invalid punctuation\n [!"\'%&(*;+~|] # - Another selector\n | /\\* # - A block comment\n)', + name: 'entity.other.attribute-name.class.css', + }, + { + captures: { + '1': { + name: 'punctuation.definition.entity.css', + }, + '2': { + patterns: [ + { + include: '#escapes', + }, + ], + }, + }, + match: + '(?x)\n(\\#)\n(\n -?\n (?![0-9])\n (?:[-a-zA-Z0-9_]|[^\\x00-\\x7F]|\\\\(?:[0-9a-fA-F]{1,6}|.))+\n)\n(?=$|[\\s,.\\#)\\[:{>+~|]|/\\*)', + name: 'entity.other.attribute-name.id.css', + }, + { + begin: '\\[', + beginCaptures: { + '0': { + name: 'punctuation.definition.entity.begin.bracket.square.css', + }, + }, + end: '\\]', + endCaptures: { + '0': { + name: 'punctuation.definition.entity.end.bracket.square.css', + }, + }, + name: 'meta.attribute-selector.css', + patterns: [ + { + include: '#comment-block', + }, + { + include: '#string', + }, + { + captures: { + '1': { + name: 'storage.modifier.ignore-case.css', + }, + }, + match: '(?<=["\'\\s]|^|\\*/)\\s*([iI])\\s*(?=[\\s\\]]|/\\*|$)', + }, + { + captures: { + '1': { + name: 'string.unquoted.attribute-value.css', + patterns: [ + { + include: '#escapes', + }, + ], + }, + }, + match: '(?x)(?<==)\\s*((?!/\\*)(?:[^\\\\"\'\\s\\]]|\\\\.)+)', + }, + { + include: '#escapes', + }, + { + match: '[~|^$*]?=', + name: 'keyword.operator.pattern.css', + }, + { + match: '\\|', + name: 'punctuation.separator.css', + }, + { + captures: { + '1': { + name: 'entity.other.namespace-prefix.css', + patterns: [ + { + include: '#escapes', + }, + ], + }, + }, + match: + "(?x)\n# Qualified namespace prefix\n( -?(?!\\d)(?:[\\w-]|[^\\x00-\\x7F]|\\\\(?:[0-9a-fA-F]{1,6}|.))+\n| \\*\n)\n# Lookahead to ensure there's a valid identifier ahead\n(?=\n \\| (?!\\s|=|$|\\])\n (?: -?(?!\\d)\n | [\\\\\\w-]\n | [^\\x00-\\x7F]\n )\n)", + }, + { + captures: { + '1': { + name: 'entity.other.attribute-name.css', + patterns: [ + { + include: '#escapes', + }, + ], + }, + }, + match: + '(?x)\n(-?(?!\\d)(?>[\\w-]|[^\\x00-\\x7F]|\\\\(?:[0-9a-fA-F]{1,6}|.))+)\n\\s*\n(?=[~|^\\]$*=]|/\\*)', + }, + ], + }, + { + include: '#pseudo-classes', + }, + { + include: '#pseudo-elements', + }, + { + include: '#functional-pseudo-classes', + }, + { + match: + '(?x) (?\\s,.\\#|){:\\[]|/\\*|$)', + name: 'entity.name.tag.css', + }, + 'unicode-range': { + captures: { + '0': { + name: 'constant.other.unicode-range.css', + }, + '1': { + name: 'punctuation.separator.dash.unicode-range.css', + }, + }, + match: '(? ? 'csharp' : language.startsWith('vue') ? 'vue' - : ['svelte', 'malina', 'riot'].includes(language) - ? ('razor' as Language) // avoid mixing code between markup & script editors when formatting - : mapLanguage(language); + : language === 'ripple' + ? 'ripple' + : ['svelte', 'malina', 'riot'].includes(language) + ? ('razor' as Language) // avoid mixing code between markup & script editors when formatting + : mapLanguage(language); try { (window as any).monaco = (window as any).monaco || (await loadMonaco()).monaco; @@ -254,6 +260,7 @@ export const createEditor = async (options: EditorOptions): Promise astro: baseUrl + '{{hash:monaco-lang-astro.js}}', clio: baseUrl + '{{hash:monaco-lang-clio.js}}', imba: baseUrl + '{{hash:monaco-lang-imba.js}}', + ripple: baseUrl + '{{hash:monaco-lang-ripple.js}}', // sql: baseUrl + '{{hash:monaco-lang-sql.js}}', // TODO: add autocomplete wat: baseUrl + '{{hash:monaco-lang-wat.js}}', }; @@ -275,6 +282,54 @@ export const createEditor = async (options: EditorOptions): Promise setTheme(currentTheme, currentEditorTheme); }; + async function registerFromTextMate( + langs: Array<{ + name: string; + scopeName: string; + syntax: string; + }>, + ) { + await addVueSupport(); // a workaround for TextMate syntax + + const { loadWASM } = await import(onigasmUrl); + const { Registry } = await import(monacoTextmateUrl); + const { wireTmGrammars } = await import(monacoEditorTextmateUrl); + + await loadWASM(onigasmWasmUrl); + + const registry = new Registry({ + getGrammarDefinition: async (scopeName: string) => ({ + format: 'json', + content: langs.find((l) => l.scopeName === scopeName)?.syntax ?? '', + }), + }); + + const grammars = new Map(); + for (const { name, scopeName } of langs) { + grammars.set(name, scopeName); + monaco.languages.register({ id: name }); + } + await wireTmGrammars(monaco, registry, grammars, editor); + } + + const addRippleSupport = async (syntaxes: { + ripple: Record; + css: Record; + }) => { + await registerFromTextMate([ + { + name: 'ripple', + scopeName: 'source.ripple', + syntax: JSON.stringify(syntaxes.ripple), + }, + { + name: 'CSS', + scopeName: 'source.css', + syntax: JSON.stringify(syntaxes.css), + }, + ]); + }; + const loadMonacoLanguage = async (lang: Language) => { if (monacoMapLanguage(lang) === 'vue') { await addVueSupport(); @@ -290,6 +345,10 @@ export const createEditor = async (options: EditorOptions): Promise if (mod.tokens) { monaco.languages.setMonarchTokensProvider(lang, mod.tokens); } + if (lang === 'ripple') { + await addRippleSupport({ ripple: (mod as any).syntax, css: (mod as any).cssSyntax }); + return; + } } }; diff --git a/src/livecodes/formatter/format.worker.ts b/src/livecodes/formatter/format.worker.ts index a6769c2bd9..99aaa06d56 100644 --- a/src/livecodes/formatter/format.worker.ts +++ b/src/livecodes/formatter/format.worker.ts @@ -1,6 +1,6 @@ import { defaultConfig } from '../config/default-config'; import { languages, parserPlugins, prettierUrl } from '../languages'; -import type { FormatFn, FormatterConfig, Language, Parser } from '../models'; +import type { Config, FormatFn, FormatterConfig, Language, Parser } from '../models'; import type { FormatterMessage, FormatterMessageEvent } from './models'; const worker: Worker = self as any; @@ -9,6 +9,7 @@ declare const prettierPlugins: { [key: string]: { parsers: any } }; declare const importScripts: (...args: string[]) => void; let baseUrl: string; +let initialConfig: Config; const parsers: { [key: string]: Parser } = {}; const plugins: { [key: string]: any } = {}; const formatters: { [key: string]: FormatFn } = {}; @@ -46,7 +47,7 @@ const load = (languages: Language[]) => { } }; -function loadParser(language: Language): Parser | undefined { +async function loadParser(language: Language): Promise { if (!(self as any).prettier) { loadPrettier(); } @@ -60,26 +61,33 @@ function loadParser(language: Language): Parser | undefined { if (!(self as any).prettierPlugins) { (self as any).prettierPlugins = {}; } - parser.plugins = parser.pluginUrls - .map((pluginUrl) => { - if (plugins[pluginUrl]) return true; - try { - importScripts(pluginUrl); - plugins[pluginUrl] = true; - if (!prettierPlugins.pug && (self as any).pluginPug) { - prettierPlugins.pug = (self as any).pluginPug; + parser.plugins = ( + await Promise.all( + parser.pluginUrls.map(async (pluginUrl) => { + if (plugins[pluginUrl]) return true; + if (language === 'ripple') { + const p = await import(pluginUrl); + prettierPlugins[language] = p; + return true; } - if (!prettierPlugins.java && (self as any).pluginJava?.default) { - prettierPlugins.java = (self as any).pluginJava.default; + try { + importScripts(pluginUrl); + plugins[pluginUrl] = true; + if (!prettierPlugins.pug && (self as any).pluginPug) { + prettierPlugins.pug = (self as any).pluginPug; + } + if (!prettierPlugins.java && (self as any).pluginJava?.default) { + prettierPlugins.java = (self as any).pluginJava.default; + } + return true; + } catch { + // eslint-disable-next-line no-console + console.warn('Failed to load formatter for: ' + language); + return false; } - return true; - } catch (err) { - // eslint-disable-next-line no-console - console.warn('Failed to load formatter for: ' + language); - return false; - } - }) - .filter(Boolean); + }), + ) + ).filter(Boolean); if (parser.plugins.length > 0) { parsers[language] = parser; @@ -87,7 +95,7 @@ function loadParser(language: Language): Parser | undefined { return parser; } -const loadFormatter = (language: Language): FormatFn | undefined => { +const loadFormatter = async (language: Language): Promise => { if (language in formatters) { return formatters[language]; } @@ -95,7 +103,7 @@ const loadFormatter = (language: Language): FormatFn | undefined => { const formatter = getFormatter(language); if (!formatter) return; - formatters[language] = formatter.factory(baseUrl, language); + formatters[language] = await formatter.factory(baseUrl, language, initialConfig); return formatters[language]; }; @@ -108,7 +116,7 @@ const format = async ( const unFormatted = { formatted: value, cursorOffset }; if (getParser(language) != null) { - const parser = loadParser(language); + const parser = await loadParser(language); const options = { useTabs: formatterConfig.useTabs ?? defaultConfig.useTabs, tabWidth: formatterConfig.tabSize ?? defaultConfig.tabSize, @@ -116,17 +124,19 @@ const format = async ( singleQuote: formatterConfig.singleQuote ?? defaultConfig.singleQuote, trailingComma: formatterConfig.trailingComma === false ? 'none' : 'all', }; - return ( - (await (self as any).prettier.formatWithCursor(value, { - parser: parser?.name, - plugins: prettierPlugins, - cursorOffset, - ...options, - })) || unFormatted - ); + let formatted = await (self as any).prettier.formatWithCursor(value, { + parser: parser?.name, + plugins: prettierPlugins, + cursorOffset, + ...options, + }); + if (typeof parser?.postFormat === 'function') { + formatted = await parser.postFormat(formatted); + } + return formatted || unFormatted; } if (getFormatter(language) != null) { - const formatFn = loadFormatter(language); + const formatFn = await loadFormatter(language); const result = await formatFn?.(value, cursorOffset); return result || unFormatted; } @@ -139,7 +149,8 @@ worker.addEventListener( const message = event.data; if (message.type === 'init') { - baseUrl = message.baseUrl; + baseUrl = message.payload.baseUrl; + initialConfig = message.payload.config; } if (message.type === 'load') { diff --git a/src/livecodes/formatter/formatter.ts b/src/livecodes/formatter/formatter.ts index 82e0441558..f9a24737c8 100644 --- a/src/livecodes/formatter/formatter.ts +++ b/src/livecodes/formatter/formatter.ts @@ -1,14 +1,14 @@ -import type { FormatFn, Language } from '../models'; +import type { Config, FormatFn, Language } from '../models'; import { getAppCDN } from '../services/modules'; import type { Formatter, FormatterMessage, FormatterMessageEvent } from './models'; -export const createFormatter = (baseUrl: string): Formatter => { +export const createFormatter = (baseUrl: string, config: Config): Formatter => { let worker: Worker | undefined; const initWorker = () => { if (worker) return; worker = new Worker(baseUrl + '{{hash:format.worker.js}}' + '?appCDN=' + getAppCDN()); - const configMessage: FormatterMessage = { type: 'init', baseUrl }; + const configMessage: FormatterMessage = { type: 'init', payload: { baseUrl, config } }; worker.postMessage(configMessage); }; diff --git a/src/livecodes/formatter/get-formatter.ts b/src/livecodes/formatter/get-formatter.ts index cdec830d79..31e587a10b 100644 --- a/src/livecodes/formatter/get-formatter.ts +++ b/src/livecodes/formatter/get-formatter.ts @@ -8,13 +8,13 @@ export const getFormatter = (config: Config, baseUrl: string, lazy: boolean): Fo if (readonly || mode === 'codeblock' || mode === 'result') { return createFakeFormatter(); } else if (lazy) { - return createLazyFormatter(baseUrl); + return createLazyFormatter(baseUrl, config); } else { - return createFormatter(baseUrl); + return createFormatter(baseUrl, config); } }; -const createLazyFormatter = (baseUrl: string) => { +const createLazyFormatter = (baseUrl: string, config: Config) => { const fakeFormatter = createFakeFormatter(); let formatter = fakeFormatter; @@ -34,7 +34,7 @@ const createLazyFormatter = (baseUrl: string) => { }; const loadFormatter = function () { - formatter = createFormatter(baseUrl); + formatter = createFormatter(baseUrl, config); lazyFormatter.load = formatter.load; lazyFormatter.getFormatFn = formatter.getFormatFn; lazyFormatter.destroy = formatter.destroy; diff --git a/src/livecodes/formatter/models.ts b/src/livecodes/formatter/models.ts index a384b00fd8..0fc780083a 100644 --- a/src/livecodes/formatter/models.ts +++ b/src/livecodes/formatter/models.ts @@ -1,4 +1,4 @@ -import type { FormatFn, FormatterConfig, Language } from '../models'; +import type { Config, FormatFn, FormatterConfig, Language } from '../models'; export interface Formatter { load: (languages: Language[]) => Promise; @@ -21,7 +21,10 @@ export type FormatterMessage = export interface InitMessage { type: 'init'; - baseUrl: string; + payload: { + baseUrl: string; + config: Config; + }; } export interface LoadMessage { diff --git a/src/livecodes/html/language-info.html b/src/livecodes/html/language-info.html index 8f2f43bbb9..c8a8f80999 100644 --- a/src/livecodes/html/language-info.html +++ b/src/livecodes/html/language-info.html @@ -1549,6 +1549,30 @@

Riot.js

+
+

Ripple

+
the elegant TypeScript UI framework
+ +

Ruby

Ruby running in the browser using Opal.
diff --git a/src/livecodes/i18n/locales/en/language-info.lokalise.json b/src/livecodes/i18n/locales/en/language-info.lokalise.json index 96cdb1c91c..0a2d8d3d98 100644 --- a/src/livecodes/i18n/locales/en/language-info.lokalise.json +++ b/src/livecodes/i18n/locales/en/language-info.lokalise.json @@ -760,6 +760,18 @@ "notes": "", "translation": "Riot.js" }, + "ripple.desc": { + "notes": "", + "translation": "the elegant TypeScript UI framework" + }, + "ripple.link": { + "notes": "### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n", + "translation": " Ripple official website Ripple documentation LiveCodes documentation Load starter template " + }, + "ripple.name": { + "notes": "", + "translation": "Ripple" + }, "ruby.desc": { "notes": "", "translation": "Ruby running in the browser using Opal." diff --git a/src/livecodes/i18n/locales/en/language-info.ts b/src/livecodes/i18n/locales/en/language-info.ts index fe234c34a3..175e1f4a08 100644 --- a/src/livecodes/i18n/locales/en/language-info.ts +++ b/src/livecodes/i18n/locales/en/language-info.ts @@ -328,6 +328,11 @@ const languageInfo = { link: '<1> <2>Riot.js official website <3> <4>Riot.js documentation <5> <6>Load starter template ', name: 'Riot.js', }, + ripple: { + desc: 'the elegant TypeScript UI framework', + link: '<1> <2>Ripple official website <3> <4>Ripple documentation <5> <6>LiveCodes documentation <7> <8>Load starter template ', + name: 'Ripple', + }, ruby: { desc: 'Ruby running in the browser using Opal.', link: '<1> <2>Ruby official website <3> <4>Ruby documentation <5><6>Opal official website <7> <8>Opal standard library CDN <9> <10>Learn X in Y minutes, where X=ruby <11> <12>LiveCodes Documentations <13> <14>Load starter template ', diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json index 83f8a79203..281288e576 100644 --- a/src/livecodes/i18n/locales/en/translation.lokalise.json +++ b/src/livecodes/i18n/locales/en/translation.lokalise.json @@ -2752,6 +2752,10 @@ "notes": "", "translation": "Riot.js Starter" }, + "templates.starter.ripple": { + "notes": "", + "translation": "Ripple Starter" + }, "templates.starter.ruby": { "notes": "", "translation": "Ruby Starter" diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts index 9e7810b549..99e54316bb 100644 --- a/src/livecodes/i18n/locales/en/translation.ts +++ b/src/livecodes/i18n/locales/en/translation.ts @@ -1034,6 +1034,7 @@ const translation = { reason: 'Reason Starter', rescript: 'ReScript Starter', riot: 'Riot.js Starter', + ripple: 'Ripple Starter', ruby: 'Ruby Starter', 'ruby-wasm': 'Ruby (Wasm) Starter', scheme: 'Scheme Starter', diff --git a/src/livecodes/languages/languages.ts b/src/livecodes/languages/languages.ts index 54e3479030..a75d030b7d 100644 --- a/src/livecodes/languages/languages.ts +++ b/src/livecodes/languages/languages.ts @@ -60,6 +60,7 @@ import { reason } from './reason'; import { rescript } from './rescript'; import { richtext } from './richtext'; import { riot } from './riot'; +import { ripple } from './ripple'; import { ruby } from './ruby'; import { rubyWasm } from './ruby-wasm'; import { scheme } from './scheme'; @@ -129,6 +130,7 @@ export const languages: LanguageSpecs[] = [ solidTsx, riot, malina, + ripple, coffeescript, livescript, civet, diff --git a/src/livecodes/languages/ripple/index.ts b/src/livecodes/languages/ripple/index.ts new file mode 100644 index 0000000000..ace98a13ca --- /dev/null +++ b/src/livecodes/languages/ripple/index.ts @@ -0,0 +1 @@ +export * from './lang-ripple'; diff --git a/src/livecodes/languages/ripple/lang-ripple-compiler.ts b/src/livecodes/languages/ripple/lang-ripple-compiler.ts new file mode 100644 index 0000000000..b05bdcc3ce --- /dev/null +++ b/src/livecodes/languages/ripple/lang-ripple-compiler.ts @@ -0,0 +1,101 @@ +import { compileBlocks } from '../../compiler/compile-blocks'; +import { createImportMap, replaceSFCImports } from '../../compiler/import-map'; +import { getCompileResult } from '../../compiler/utils'; +import type { CompilerFunction, Config } from '../../models'; +import { modulesService } from '../../services/modules'; +import { pkgInfoService } from '../../services/pkgInfo'; +import { getLanguageByAlias } from '../utils'; + +(self as any).createRippleCompiler = async (initialConfig: Config): Promise => { + const MAIN_FILE = 'Component.ripple'; + let importedContent = ''; + let imports: Record = {}; + + const getModuleName = (v: string) => + v.startsWith('pr:ripple@') || v.startsWith('pkg.pr.new:ripple@') + ? v // 'pr:ripple@f8bdb34' + : `ripple@${v}`; // '0.2.25' + + let version: string = + initialConfig.customSettings.ripple?.version || + (await pkgInfoService.getPkgLatestVersion('ripple')); + let compile: (code: string, filename: string) => Promise<{ js: { code: string }; css: string }>; + const updateCompiler = async (currentVersion: string) => { + if (typeof compile === 'function' && currentVersion === version) return; + const modName = getModuleName(currentVersion + '/compiler'); + const mod = await import(modulesService.getModuleUrl(modName)); + compile = mod.compile; + version = currentVersion; + }; + + const compileRipple = async ( + code: string, + { config, filename }: { config: Config; filename: string }, + ) => { + if (filename === MAIN_FILE) { + importedContent = ''; + imports = {}; + } + if (!code.trim()) return getCompileResult(''); + + const newVersion = config.customSettings.ripple?.version || version; + await updateCompiler(newVersion); + + const isRipple = (mod: string) => + mod.toLowerCase().endsWith('.ripple') || mod.toLowerCase().startsWith('data:text/ripple'); + + const fullCode = await replaceSFCImports(code, { + config, + filename, + getLanguageByAlias, + isSfc: isRipple, + compileSFC: async ( + code: string, + { config, filename }: { config: Config; filename: string }, + ) => { + const compiled = (await compileRipple(code, { config, filename })).code; + importedContent += `\n${filename}\n\n${code}\n`; + return compiled; + }, + }); + const processedCode = await compileBlocks(fullCode, 'style', config, { + ignoreComments: true, + }); + const { js, css } = await compile(processedCode, filename); + + const cssCode = + css === '' + ? '' + : ` +const styles = document.createElement('style'); +styles.innerHTML = ${JSON.stringify(css)}; +document.head.appendChild(styles); +`; + + if (filename === MAIN_FILE) { + const moduleUrl = modulesService.getUrl(getModuleName(version)); + imports = { + ...createImportMap(importedContent, config), + ripple: `${moduleUrl}/src/runtime/index-client.js`, + 'ripple/internal/client': `${moduleUrl}/src/runtime/internal/client/index.js`, + 'ripple/jsx-runtime': `${moduleUrl}/src/jsx-runtime.js`, + [`${moduleUrl}/src/runtime/internal/client/constants`]: `${moduleUrl}/src/runtime/internal/client/constants.js`, + [`${moduleUrl}/src/runtime/internal/client/runtime`]: `${moduleUrl}/src/runtime/internal/client/runtime.js`, + clsx: modulesService.getModuleUrl('clsx'), + devalue: modulesService.getModuleUrl('devalue'), + 'esm-env': modulesService.getModuleUrl('esm-env'), + }; + } + + return { + code: `${js.code}\n${cssCode}`, + info: { importedContent, imports }, + }; + }; + + return (code, { config }) => + compileRipple(code, { + config, + filename: MAIN_FILE, + }); +}; diff --git a/src/livecodes/languages/ripple/lang-ripple-formatter.ts b/src/livecodes/languages/ripple/lang-ripple-formatter.ts new file mode 100644 index 0000000000..acfd929041 --- /dev/null +++ b/src/livecodes/languages/ripple/lang-ripple-formatter.ts @@ -0,0 +1,28 @@ +import { defaultConfig } from '../../config/default-config'; +import type { Config, FormatFn } from '../../models'; +import { modulesService } from '../../services/modules'; +import { pkgInfoService } from '../../services/pkgInfo'; +import { prettierEsmUrl } from '../../vendors'; + +(self as any).createRippleFormatter = async (initialConfig: Config): Promise => { + const version = + initialConfig.customSettings?.ripple?.version || + (await pkgInfoService.getPkgLatestVersion('ripple')); + const pluginUrl = + version.startsWith('pr:ripple@') || version.startsWith('pkg.pr.new:ripple@') + ? modulesService.getModuleUrl(version.replace('ripple', '@ripple-ts/prettier-plugin')) + : modulesService.getModuleUrl('@ripple-ts/prettier-plugin@' + version); + + const [prettier, ripplePlugin] = await Promise.all([import(prettierEsmUrl), import(pluginUrl)]); + return async (code, cursorOffset, formatterConfig) => + prettier.formatWithCursor(code, { + parser: 'ripple', + plugins: [ripplePlugin], + cursorOffset, + useTabs: formatterConfig?.useTabs ?? defaultConfig.useTabs, + tabWidth: formatterConfig?.tabSize ?? defaultConfig.tabSize, + semi: formatterConfig?.semicolons ?? defaultConfig.semicolons, + singleQuote: formatterConfig?.singleQuote ?? defaultConfig.singleQuote, + trailingComma: formatterConfig?.trailingComma === false ? 'none' : 'all', + }); +}; diff --git a/src/livecodes/languages/ripple/lang-ripple.ts b/src/livecodes/languages/ripple/lang-ripple.ts new file mode 100644 index 0000000000..88289925c6 --- /dev/null +++ b/src/livecodes/languages/ripple/lang-ripple.ts @@ -0,0 +1,21 @@ +import type { LanguageSpecs } from '../../models'; + +export const ripple: LanguageSpecs = { + name: 'ripple', + title: 'Ripple', + formatter: { + factory: async (baseUrl, _language, config) => { + (self as any).importScripts(baseUrl + '{{hash:lang-ripple-formatter.js}}'); + return (self as any).createRippleFormatter(config); + }, + }, + compiler: { + factory: (config, baseUrl) => { + (self as any).importScripts(baseUrl + '{{hash:lang-ripple-compiler.js}}'); + return (self as any).createRippleCompiler(config); + }, + }, + extensions: ['ripple'], + editor: 'script', + editorLanguage: 'jsx', +}; diff --git a/src/livecodes/languages/ripple/ripple-runtime.ts b/src/livecodes/languages/ripple/ripple-runtime.ts new file mode 100644 index 0000000000..80653677e6 --- /dev/null +++ b/src/livecodes/languages/ripple/ripple-runtime.ts @@ -0,0 +1,9 @@ +export const rippleRuntime = ` +import { mount } from 'ripple'; +import App from "./script"; +(() => { + mount(App, { + target: document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement('div')), + }); +})(); +`; diff --git a/src/livecodes/result/result-page.ts b/src/livecodes/result/result-page.ts index c269305b90..6d98d66344 100644 --- a/src/livecodes/result/result-page.ts +++ b/src/livecodes/result/result-page.ts @@ -11,6 +11,7 @@ import { import { cssPresets, getLanguageCompiler, getLanguageExtension } from '../languages'; import { reactRuntime } from '../languages/jsx/react-runtime'; import { reactNativeRuntime } from '../languages/react-native/react-native-runtime'; +import { rippleRuntime } from '../languages/ripple/ripple-runtime'; import { solidRuntime } from '../languages/solid/solid-runtime'; import { hasCustomJsxRuntime } from '../languages/typescript'; import type { Cache, CompileInfo, Config, EditorId, Language } from '../models'; @@ -217,6 +218,7 @@ export const createResultPage = async ({ 'react-native-tsx': reactNativeRuntime, solid: solidRuntime, 'solid.tsx': solidRuntime, + ripple: rippleRuntime, }; const jsxRuntime = jsxRuntimes[code.script.language] || ''; const reactImport = @@ -446,7 +448,7 @@ export const createResultPage = async ({ } } - // React JSX runtime + // JSX runtime if (shouldInsertJsxRuntime) { const jsxRuntimeScript = dom.createElement('script'); jsxRuntimeScript.type = 'module'; diff --git a/src/livecodes/services/pkgInfo.ts b/src/livecodes/services/pkgInfo.ts index 5096143cb3..41b36f493c 100644 --- a/src/livecodes/services/pkgInfo.ts +++ b/src/livecodes/services/pkgInfo.ts @@ -217,9 +217,17 @@ const getPkgDefaultFiles = async ( }; }; +const getPkgLatestVersion = async (pkgName: string): Promise => { + const pkg = await fetch(`${apiEndpoint}/packages/npm/${pkgName}`, { headers: jsDelivrHeaders }) + .then((res) => res.json()) + .catch(() => ({})); + return pkg.tags?.latest || 'latest'; +}; + export const pkgInfoService: CDNService = { search, getPkgInfo, getPkgFiles, getPkgDefaultFiles, + getPkgLatestVersion, }; diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts index 9f8a22eb10..4d7b358570 100644 --- a/src/livecodes/templates/starter/index.ts +++ b/src/livecodes/templates/starter/index.ts @@ -53,6 +53,7 @@ import { reactStarter } from './react-starter'; import { reasonStarter } from './reason-starter'; import { rescriptStarter } from './rescript-starter'; import { riotStarter } from './riot-starter'; +import { rippleStarter } from './ripple-starter'; import { rubyStarter } from './ruby-starter'; import { rubyWasmStarter } from './ruby-wasm-starter'; import { schemeStarter } from './scheme-starter'; @@ -90,6 +91,7 @@ export const starterTemplates = [ astroStarter, riotStarter, malinaStarter, + rippleStarter, jqueryStarter, backboneStarter, knockoutStarter, diff --git a/src/livecodes/templates/starter/ripple-starter.ts b/src/livecodes/templates/starter/ripple-starter.ts new file mode 100644 index 0000000000..bbe8451d88 --- /dev/null +++ b/src/livecodes/templates/starter/ripple-starter.ts @@ -0,0 +1,50 @@ +import type { Template } from '../../models'; + +export const rippleStarter: Template = { + name: 'ripple', + title: window.deps.translateString('templates.starter.ripple', 'Ripple Starter'), + thumbnail: 'assets/templates/ripple.png', + activeEditor: 'script', + markup: { + language: 'html', + content: '', + }, + style: { + language: 'css', + content: '', + }, + script: { + language: 'ripple', + content: ` +import { track } from 'ripple'; + +component Counter(props: { name: string }) { +
    +

    {\`Hello, \${props.@name}!\`}

    + + + const count = track(0); +

    {\`You clicked \${@count} times.\`}

    + +
    + + +} + +export default component App() { + const name = track("World"); + setTimeout(() => @name = "Ripple", 2000); + +} +`.trimStart(), + }, +}; diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index b09950faa6..183990be43 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -297,8 +297,12 @@ export const mjmlUrl = /* @__PURE__ */ getUrl('mjml-browser@4.15.3/lib/index.js' export const monacoBaseUrl = /* @__PURE__ */ getUrl('@live-codes/monaco-editor@0.3.0/'); +export const monacoEditorTextmateUrl = /* @__PURE__ */ getModuleUrl('monaco-editor-textmate@4.0.0'); + export const monacoEmacsUrl = /* @__PURE__ */ getUrl('monaco-emacs@0.3.0/dist/monaco-emacs.js'); +export const monacoTextmateUrl = /* @__PURE__ */ getModuleUrl('monaco-textmate@3.0.1'); + export const monacoThemesBaseUrl = /* @__PURE__ */ getUrl('monaco-themes@0.4.4/themes/'); export const monacoVimUrl = /* @__PURE__ */ getUrl('monaco-vim@0.4.1/dist/monaco-vim.js'); @@ -317,6 +321,10 @@ export const normalizeCssUrl = /* @__PURE__ */ getUrl('normalize.css@8.0.1/norma export const nunjucksBaseUrl = /* @__PURE__ */ getUrl('nunjucks@3.2.4/browser/'); +export const onigasmUrl = /* @__PURE__ */ getModuleUrl('onigasm@2.2.5'); + +export const onigasmWasmUrl = /* @__PURE__ */ getUrl('onigasm@2.2.5/lib/onigasm.wasm'); + export const opalBaseUrl = /* @__PURE__ */ getUrl('https://cdn.opalrb.com/opal/1.8.2/'); export const parinferUrl = /* @__PURE__ */ getUrl('parinfer@3.13.1/parinfer.js'); @@ -337,6 +345,8 @@ export const postcssImportUrlUrl = /* @__PURE__ */ getUrl( export const prettierBaseUrl = /* @__PURE__ */ getUrl('prettier@3.3.2/'); +export const prettierEsmUrl = /* @__PURE__ */ getModuleUrl('prettier@3.3.2/standalone'); + export const prettierPhpUrl = /* @__PURE__ */ getUrl('@prettier/plugin-php@0.22.2/standalone.js'); export const prismBaseUrl = /* @__PURE__ */ getUrl('prismjs@1.29.0/components/'); diff --git a/src/sdk/models.ts b/src/sdk/models.ts index adebd1a1ae..ea8a23caa9 100644 --- a/src/sdk/models.ts +++ b/src/sdk/models.ts @@ -1247,6 +1247,7 @@ export type ParserName = | 'babel-ts' | 'babel-flow' | 'glimmer' + | 'ripple' | 'html' | 'markdown' | 'css' @@ -1260,6 +1261,10 @@ export interface Parser { name: ParserName; plugins?: any[]; pluginUrls: string[]; + postFormat?: (parsed: { + formatted: string; + cursorOffset: number; + }) => Promise<{ formatted: string; cursorOffset: number }>; } export type FormatFn = ( value: string, @@ -1268,7 +1273,7 @@ export type FormatFn = ( ) => Promise<{ formatted: string; cursorOffset: number }>; export interface LanguageFormatter { - factory: (baseUrl: string, language: Language) => FormatFn; + factory: (baseUrl: string, language: Language, config: Config) => FormatFn | Promise; } export type CssPresetId = '' | 'normalize.css' | 'reset-css'; @@ -1392,6 +1397,7 @@ export type TemplateName = | 'preact' | 'svelte' | 'solid' + | 'ripple' | 'lit' | 'stencil' | 'mdx' @@ -2078,6 +2084,7 @@ export interface CDNService { getPkgInfo: (pkgName: string) => Promise; getPkgFiles: (pkgName: string) => Promise<{ default?: string; files: string[] } | APIError>; getPkgDefaultFiles: (pkgName: string) => Promise<{ js?: string; css?: string } | APIError>; + getPkgLatestVersion: (pkgName: string) => Promise; } export interface WorkerMessageEvent extends MessageEvent { diff --git a/typos.toml b/typos.toml index 629bd22d2b..b71e8ea5b4 100644 --- a/typos.toml +++ b/typos.toml @@ -8,6 +8,7 @@ extend-exclude = [ "src/livecodes/services/google-fonts.ts", # Font name "src/livecodes/templates/starter/blockly-starter.ts", # Random string id "functions/vendors/*.js", # Built files + "src/livecodes/editor/monaco/languages/monaco-lang-ripple.ts" ] [default]