From f2dba93c7a6f33567ca53f4c624944c831735e39 Mon Sep 17 00:00:00 2001 From: Dmitry Vasilev Date: Sun, 2 Jul 2023 20:22:41 +0300 Subject: [PATCH] examples --- index.html | 6 -- src/cmd.js | 17 ++++-- src/editor/domutils.js | 2 +- src/editor/files.js | 124 ++++++++++++++++++++++++----------------- src/effects.js | 9 ++- src/examples.js | 57 +++++++++++++++++++ src/filesystem.js | 11 ++-- src/index.js | 58 ++++++++++++++----- 8 files changed, 197 insertions(+), 87 deletions(-) create mode 100644 src/examples.js diff --git a/index.html b/index.html index 14a374f..f633ea8 100644 --- a/index.html +++ b/index.html @@ -200,16 +200,10 @@ } .allow_file_access { - height: 100%; - padding: 10px; display: flex; flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; } .allow_file_access .subtitle { - padding: 10px; font-size: 0.8em; } diff --git a/src/cmd.js b/src/cmd.js index bafde88..57d4284 100644 --- a/src/cmd.js +++ b/src/cmd.js @@ -808,7 +808,7 @@ const clear_io_trace = state => { return run_code({...state, io_trace: null}) } -const do_load_dir = (state, dir) => { +const load_files = (state, dir) => { const collect_files = dir => dir.kind == 'file' ? [dir] : dir.children.map(collect_files).flat() @@ -820,7 +820,7 @@ const do_load_dir = (state, dir) => { return { ...state, project_dir: dir, - files: {...files, ...state.files}, + files: {...files, '': state.files['']}, } } @@ -842,24 +842,29 @@ const apply_entrypoint_settings = (state, entrypoint_settings) => { } } -const load_dir = (state, dir, entrypoint_settings) => { +const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => { // Clear parse cache and rerun code - const with_dir = do_load_dir(state, dir) + const with_dir = load_files(state, dir) return run_code({ ...( entrypoint_settings == null ? with_dir : apply_entrypoint_settings(with_dir, entrypoint_settings) ), + + has_file_system_access, + // remove cache. We have to clear cache because imports of modules that are // not available because project_dir is not available have errors and the // errors are cached parse_result: null, + + external_imports_cache: null, }) } const create_file = (state, dir, current_module) => { - return {...load_dir(state, dir), current_module} + return {...load_dir(state, dir, true), current_module} } const open_run_window = (state, globals) => { @@ -877,7 +882,7 @@ const open_run_window = (state, globals) => { const get_initial_state = (state, entrypoint_settings) => { const with_files = state.project_dir == null ? state - : do_load_dir(state, state.project_dir) + : load_files(state, state.project_dir) const with_settings = apply_entrypoint_settings(with_files, entrypoint_settings) diff --git a/src/editor/domutils.js b/src/editor/domutils.js index 7e53282..9558319 100644 --- a/src/editor/domutils.js +++ b/src/editor/domutils.js @@ -21,7 +21,7 @@ export function el(tag, className, ...children){ const append = child => { if(typeof(child) == 'undefined') { throw new Error('illegal state') - } else if(child !== null) { + } else if(child !== null && child !== false) { result.appendChild( typeof(child) == 'string' ? document.createTextNode(child) diff --git a/src/editor/files.js b/src/editor/files.js index 6931224..3fe0c79 100644 --- a/src/editor/files.js +++ b/src/editor/files.js @@ -1,7 +1,13 @@ import {el} from './domutils.js' import {map_find} from '../utils.js' -import {load_dir, create_file} from '../filesystem.js' -import {exec, get_state, open_directory, reload_run_window} from '../index.js' +import {open_dir, create_file} from '../filesystem.js' +import { + exec, + get_state, + open_directory, + reload_run_window, + close_directory, +} from '../index.js' const is_html = path => path.endsWith('.htm') || path.endsWith('.html') const is_js = path => path == '' || path.endsWith('.js') || path.endsWith('.mjs') @@ -25,27 +31,56 @@ export class Files { reload_run_window(get_state()) } - render(state) { - if(state.project_dir == null) { - this.el.innerHTML = '' - this.el.appendChild( - el('div', 'allow_file_access', - el('a', { - href: 'javascript:void(0)', - click: open_directory, - }, - `Allow access to local project folder`, - ), - el('div', 'subtitle', `Your files will never leave your device`) - ) - ) - } else { - this.render_files(state) - } - } - render_files(state) { - const children = [ + render(state) { + const file_actions = state.has_file_system_access + ? el('div', 'file_actions', + el('a', { + 'class': 'file_action', + href: 'javascript: void(0)', + click: this.create_file.bind(this, false), + }, + 'New file' + ), + + el('a', { + 'class': 'file_action', + href: 'javascript: void(0)', + click: this.create_file.bind(this, true), + }, + 'New dir' + ), + + el('a', { + 'class': 'file_action', + href: 'javascript: void(0)', + click: close_directory, + }, + 'Revoke access' + ), + + el('a', { + href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module', + target: '__blank', + "class": 'select_entrypoint_title', + title: 'Select entrypoint', + }, + 'Entry point' + ), + ) + : el('div', 'file_actions', + el('div', 'file_action allow_file_access', + el('a', { + href: 'javascript: void(0)', + click: open_directory, + }, 'Allow access to local project folder'), + el('span', 'subtitle', `Your files will never leave your device`) + ), + ) + + + + const file_elements = [ this.render_file({name: '*scratch*', path: ''}, state), this.render_file(state.project_dir, state), ] @@ -54,41 +89,19 @@ export class Files { if(files == null) { this.el.innerHTML = '' + this.el.appendChild(file_actions) this.el.appendChild( - el('div', 'file_actions', - el('a', { - 'class': 'file_action', - href: 'javascript: void(0)', - click: this.create_file.bind(this, false), - }, - 'Create file' - ), - el('a', { - 'class': 'file_action', - href: 'javascript: void(0)', - click: this.create_file.bind(this, true), - }, 'Create dir'), - el('a', { - href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module', - target: '__blank', - "class": 'select_entrypoint_title', - title: 'Select entrypoint', - }, 'Entry point'), - ) - ) - this.el.appendChild( - el('div', 'files', - children - ) + el('div', 'files', file_elements) ) } else { // Replace to preserve scroll position - files.replaceChildren(...children) + this.el.replaceChild(file_actions, this.el.children[0]) + files.replaceChildren(...file_elements) } } render_select_entrypoint(file, state) { - if(file.kind == 'directory') { + if(!state.has_file_system_access || file.kind == 'directory') { return null } else if(is_js(file.path)) { return el('span', 'select_entrypoint', @@ -189,9 +202,9 @@ export class Files { await create_file(path, is_dir) // Reload all files for simplicity - load_dir(false).then(dir => { + open_dir(false).then(dir => { if(is_dir) { - exec('load_dir', dir) + exec('load_dir', dir, true) } else { exec('create_file', dir, path) } @@ -205,8 +218,15 @@ export class Files { this.active_el = e.currentTarget.parentElement e.currentTarget.classList.add('active') this.active_file = file + if(file.kind != 'directory') { - exec('change_current_module', file.path) + if(get_state().has_file_system_access) { + exec('change_current_module', file.path) + } else { + // in examples mode, on click file we also change entrypoint for + // simplicity + exec('change_entrypoint', file.path) + } } } } diff --git a/src/effects.js b/src/effects.js index 629ce6f..531d9d5 100644 --- a/src/effects.js +++ b/src/effects.js @@ -1,4 +1,5 @@ import {write_file} from './filesystem.js' +import {write_example} from './examples.js' import {color_file} from './color.js' import { root_calltree_node, @@ -293,6 +294,12 @@ export const EFFECTS = { localStorage[key] = value }, - write: (state, [name, contents], ui) => write_file(name, contents), + write: (state, [name, contents], ui) => { + if(state.has_file_system_access) { + write_file(name, contents) + } else { + write_example(name, contents) + } + } } diff --git a/src/examples.js b/src/examples.js new file mode 100644 index 0000000..c2b2a0c --- /dev/null +++ b/src/examples.js @@ -0,0 +1,57 @@ +export const write_example = (name, contents) => { + localStorage['examples_' + name] = contents +} + +const read_example = name => { + return localStorage['examples_' + name] +} + +const list = [ + 'github_api/index.js', + 'ethers/block_by_timestamp.js', + 'ethers/index.js', + // TODO for html5 example, open run window or hint that it should be opened +] +.map(l => l.split('/')) + + +const get_children = path => { + const children = list.filter(l => path.every((elem, i) => elem == l[i] )) + const files = children.filter(c => c.length == path.length + 1) + const dirs = [...new Set(children + .filter(c => c.length != path.length + 1) + .map(c => c[path.length]) + )] + return Promise.all(files.map(async f => { + const name = f[path.length] + const filepath = f.slice(0, path.length + 1).join('/') + return { + name, + path: filepath, + kind: 'file', + contents: + read_example(filepath) ?? + await fetch(globalThis.location.origin + '/docs/examples/'+ filepath) + .then(r => r.text()), + } + }) + .concat(dirs.map(async d => { + const p = [...path, d] + return { + name: d, + path: p.join('/'), + kind: 'directory', + children: await get_children(p), + } + }))) +} + +export const examples_promise = get_children([]).then(children => { + return { + kind: 'directory', + name: 'examples', + path: null, + children, + } +}) + diff --git a/src/filesystem.js b/src/filesystem.js index a7bc2b0..b865620 100644 --- a/src/filesystem.js +++ b/src/filesystem.js @@ -15,11 +15,10 @@ const send_message = (message) => { }); } -globalThis.clear_directory_handle = () => { +export const close_dir = () => { send_message({type: 'SET_DIR_HANDLE', data: null}) clearInterval(keepalive_interval_id) keepalive_interval_id = null - window.location.reload() } let dir_handle @@ -120,7 +119,7 @@ const read_file = async handle => { return await file_data.text() } -const do_load_dir = async (handle, path) => { +const do_open_dir = async (handle, path) => { if(handle.kind == 'directory') { const children = [] for await (let [name, h] of handle) { @@ -134,7 +133,7 @@ const do_load_dir = async (handle, path) => { kind: 'directory', children: (await Promise.all( children.map(c => - do_load_dir(c, path == null ? c.name : path + '/' + c.name) + do_open_dir(c, path == null ? c.name : path + '/' + c.name) ) )).sort((a, b) => a.name.localeCompare(b.name)) } @@ -159,7 +158,7 @@ export const create_file = (path, is_dir) => { ) } -export const load_dir = async (should_request_access) => { +export const open_dir = async (should_request_access) => { let handle if(should_request_access) { handle = await request_directory_handle() @@ -171,5 +170,5 @@ export const load_dir = async (should_request_access) => { } else { keep_service_worker_alive() } - return do_load_dir(handle, null) + return do_open_dir(handle, null) } diff --git a/src/index.js b/src/index.js index d48c475..89f3b7c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,22 @@ import {UI} from './editor/ui.js' import {EFFECTS, render_initial_state, apply_side_effects} from './effects.js' -import {load_dir, init_window_service_worker} from './filesystem.js' +import { + open_dir, + close_dir, + init_window_service_worker +} from './filesystem.js' +import {examples_promise} from './examples.js' -const EXAMPLE = `const fib = n => - n == 0 || n == 1 - ? n - : fib(n - 1) + fib(n - 2) -fib(6)` +const EXAMPLE = `function fib(n) { + if(n == 0 || n == 1) { + return n + } else { + return fib(n - 1) + fib(n - 2) + } +} + +fib(6) +` const set_error_handler = (w, with_unhandled_rejection = true) => { @@ -131,38 +141,56 @@ export const reload_run_window = state => { globalThis.run_window.location = get_html_url(state) } + const read_modules = async () => { const default_module = {'': localStorage.code || EXAMPLE} - const project_dir = await load_dir(false) + const project_dir = await open_dir(false) if(project_dir == null) { - // Single anonymous module return { + project_dir: await examples_promise, files: default_module, + has_file_system_access: false, } } else { return { project_dir, files: default_module, + has_file_system_access: true, } } } -const get_entrypoint_settings = () => ({ - current_module: localStorage.current_module ?? '', - entrypoint: localStorage.entrypoint ?? '', - html_file: localStorage.html_file ?? '', -}) +const get_entrypoint_settings = () => { + + const params = new URLSearchParams(window.location.search) + + const entrypoint = null + ?? params.get('entrypoint') + ?? localStorage.entrypoint + ?? '' + + return { + current_module: params.get('entrypoint') ?? localStorage.current_module ?? '', + entrypoint, + html_file: localStorage.html_file ?? '', + } +} export const open_directory = () => { if(globalThis.showDirectoryPicker == null) { throw new Error('Your browser is not supporting File System Access API') } - load_dir(true).then(dir => { - exec('load_dir', dir, get_entrypoint_settings()) + open_dir(true).then(dir => { + exec('load_dir', dir, true, get_entrypoint_settings()) }) } +export const close_directory = async () => { + close_dir() + exec('load_dir', await examples_promise, false, get_entrypoint_settings()) +} + let COMMANDS let ui