Skip to content

Commit

Permalink
feat(Import): use files query param for file selection when importing
Browse files Browse the repository at this point in the history
  • Loading branch information
hatemhosny committed May 17, 2024
1 parent 7f6fcd0 commit b787e03
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 53 deletions.
4 changes: 4 additions & 0 deletions docs/docs/configuration/query-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ https://livecodes.io?js=console.log('Hello World!')&console=open

Alias to [`import`](../sdk/js-ts.md#import) (a URL to [import](../features/import.md)).

- `files`: `string`.

A comma-separated [list of files to import](../features/import.md#file-selection).

- `raw`: [`Language`](../api/modules/internal.md#language).

When used with `import` or `x`, imports the URL as code of the provided language.
Expand Down
46 changes: 36 additions & 10 deletions docs/docs/features/import.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Overview

LiveCodes supports importing code from a wide variety of sources.
LiveCodes supports importing code from a wide variety of [sources](#sources). This can be achieved using one of the following methods:

### UI

The Import screen can be accessed from the app menu → Import.

Expand All @@ -12,7 +14,19 @@ import RunInLiveCodes from '../../src/components/RunInLiveCodes.tsx';

![LiveCodes Import](../../static/img/screenshots/import.jpg)

Alternatively, a URL of any of the sources can be imported on-load by adding it as a value to [query param](../configuration/query-params.md) key: `x`. This is easier using the [bookmarklet](../bookmarklet.md).
### Query Param

A URL of any of the [sources](#sources) can be imported by adding it as a value to [query param](../configuration/query-params.md) key: `x`.

Example:

https://livecodes.io/?x=https://gist.github.com/f01deb828a42f363502fbae7964d48e9

### Bookmarklet

Instead of manually copy/pasting URLs to import, adding [**"Edit in LiveCodes"** bookmarklet](../bookmarklet.md) to the browser bookmarks bar can be a more convenient way. It opens LiveCodes in a new window and imports the current webpage URL.

### SDK

For [embedded playgrounds](./embeds.md), use the [SDK](../sdk/index.md) property [`EmbedOptions.import`](../sdk/js-ts.md#import).

Expand Down Expand Up @@ -75,10 +89,26 @@ For sources that provide multiple files (e.g. GitHub/GitLab directories, GitHub
- index.html, style.css, script.js
- default.pug, app.scss, main.ts

Markup files starting with `index.`, style files starting with `style.` and script files starting with `script.` are given higher priority. While Markup files starting with `readme.` are given lower priority.
The following file names are given higher priority:

Alternatively, languages and files can be specified using [query params](../configuration/query-params.md):
`?x={url}&{language1}={file1}&{language2}={file2}&{language3}={file3}`
- Markup files starting with `index.` or `default.`
- Style files starting with `style.` or `styles.`
- Script files starting with `script.`, `app.`, `main.` or `index.`

While Markup files starting with `readme.` are given lower priority.

Alternatively, files can be specified using the `files` [query param](../configuration/query-params.md). 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.md#activeeditor) (or its alias `active`) [query param](../configuration/query-params.md). 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

Expand Down Expand Up @@ -132,13 +162,9 @@ If the response text could not be parsed as DOM or no elements matched the CSS s
Alternatively, the language of raw code can be specified using [query params](../configuration/query-params.md):
`?x={url}&raw={language}`

## "Edit in LiveCodes" Bookmarklet

Instead of manually copy/pasting URLs to import, adding [**"Edit in LiveCodes"** bookmarklet](../bookmarklet.md) to the browser bookmarks bar can be a more convenient way. It opens LiveCodes in a new window and imports the current webpage URL.

## Import from CodePen

Currently, CodePen API does not allow directly importing code from Pens (except for Pens of Pro users, which can be imported!). However, you can export any saved Pen as a [zip file](https://blog.codepen.io/documentation/exporting-pens/#export-zip-1) or [Github gist](https://blog.codepen.io/documentation/exporting-pens/#save-as-github-gist-2) and then import it to LiveCodes. The format that Codepen exports is well understood by LiveCodes. Most Pens can be imported with no or minimal changes.
Currently, CodePen API does not allow directly importing code from Pens. However, you can export any saved Pen as a [zip file](https://blog.codepen.io/documentation/exporting-pens/#export-zip-1) or [Github gist](https://blog.codepen.io/documentation/exporting-pens/#save-as-github-gist-2) and then import it to LiveCodes. The format that Codepen exports is well understood by LiveCodes. Most Pens can be imported with no or minimal changes.

**Note:** External resources (styles/scripts) are not exported with source code in zip file export of CodePen. However, export to GitHub gist does export these. So if a Pen with external resources exported as zip file is not imported properly, try exporting to GitHub gist or manually add the [external resources](./external-resources.md).

Expand Down
110 changes: 67 additions & 43 deletions src/livecodes/import/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,44 @@ export const populateConfig = (
}
}

// select files based on language in query params (e.g. ?html=index.html&js=script.js)
const languagesInParams = Object.keys(params).some(getLanguageByAlias);
if (languagesInParams) {
return Object.keys(params).reduce((output: Partial<Config>, param: string) => {
const language = getLanguageByAlias(param);
if (!language) return output;
const file = files.find((file) => file?.filename === params[param]);
if (!file) return output;

const editorId = getLanguageEditorId(language);
if (!editorId || output[editorId]) return output;
// select files in query params (e.g. ?files=index.html,script.js)
const filesInParams = params.files;
if (filesInParams) {
return filesInParams
.split(',')
.map((filename) => filename.trim())
.reduce((output: Partial<Config>, filename: string) => {
const extension = filename.split('.')[filename.split('.').length - 1];
const language = getLanguageByAlias(extension);
if (!language) return output;
const file = files.find((file) => file.filename === filename);
if (!file) return output;

return {
...output,
[editorId]: {
language,
content: file.content,
},
};
}, {} as Partial<Config>);
const editorId = getLanguageEditorId(language);
if (!editorId || output[editorId]) return output;

return {
...output,
activeEditor: output.activeEditor || editorId, // set first as active editor
[editorId]: {
language,
content: file.content,
},
};
}, {} as Partial<Config>);
}

// select languages from files
const code = files
.map((file) => ({
...file,
filename: file.filename.toLowerCase(),
}))
.filter(
(file) =>
// do not import json files
!file.filename.endsWith('.json'),
)
.map((file) => {
const extension = file.filename.split('.')[file.filename.split('.').length - 1];
const language: Language = file.language || getLanguageByAlias(extension) || 'html';
Expand All @@ -58,31 +72,41 @@ export const populateConfig = (
};
})
.sort((a, b) => {
if (
// put default files first
a.editorId === b.editorId &&
((a.editorId === 'markup' && a.filename.toLowerCase().startsWith('index.')) ||
(a.editorId === 'style' && a.filename.toLowerCase().startsWith('style.')) ||
(a.editorId === 'script' && a.filename.toLowerCase().startsWith('script.')))
) {
return -1;
// put default files first
if (a.editorId === b.editorId && a.editorId === 'markup') {
// index.html -> default.html
if (a.filename.startsWith('index.')) return -1;
if (b.filename.startsWith('index.')) return 1;

if (a.filename.startsWith('default.')) return -1;
if (b.filename.startsWith('default.')) return 1;
}
if (
// put default files first
a.editorId === b.editorId &&
((b.editorId === 'markup' && b.filename.toLowerCase().startsWith('index.')) ||
(b.editorId === 'style' && b.filename.toLowerCase().startsWith('style.')) ||
(b.editorId === 'script' && b.filename.toLowerCase().startsWith('script.')))
) {
return 1;
if (a.editorId === b.editorId && a.editorId === 'style') {
// style.css -> styles.css
if (a.filename.startsWith('style.')) return -1;
if (b.filename.startsWith('style.')) return 1;

if (a.filename.startsWith('styles.')) return -1;
if (b.filename.startsWith('styles.')) return 1;
}
if (a.editorId === b.editorId && a.editorId === 'script') {
// script.js -> app.js -> main.js -> index.js
if (a.filename.startsWith('script.')) return -1;
if (b.filename.startsWith('script.')) return 1;

if (a.filename.startsWith('app.')) return -1;
if (b.filename.startsWith('app.')) return 1;

if (a.filename.startsWith('main.')) return -1;
if (b.filename.startsWith('main.')) return 1;

if (a.filename.startsWith('index.')) return -1;
if (b.filename.startsWith('index.')) return 1;
}
if (
// put readme last
a.editorId === b.editorId &&
a.editorId === 'markup'
) {
if (a.filename.toLowerCase().startsWith('readme')) return 1;
if (b.filename.toLowerCase().startsWith('readme')) return -1;
// put readme last
if (a.editorId === b.editorId && a.editorId === 'markup') {
if (a.filename.startsWith('readme')) return 1;
if (b.filename.startsWith('readme')) return -1;
}
if (a.language === b.language) {
// if same language, sort by filename
Expand All @@ -96,7 +120,7 @@ export const populateConfig = (
})
.reduce((output: Partial<Config>, file) => {
// tests
if (file.filename.toLowerCase().match(new RegExp('.(test|spec)\\.[jt]sx?'))) {
if (file.filename.match(new RegExp('.(test|spec)\\.[jt]sx?'))) {
if (output.tests?.content) return output;
return {
...output,
Expand Down
1 change: 1 addition & 0 deletions src/sdk/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,7 @@ export type UrlQueryParams = Partial<
embed: boolean;
preview: boolean;
x: string;
files: string; // comma-separated files (e.g. import from GitHub dir)
raw: Language;
language: Language;
lang: Language;
Expand Down

0 comments on commit b787e03

Please sign in to comment.