more REPL stuff

pull/1890/head
Rich Harris 7 years ago
parent 15d25b7374
commit e03ba9051c

@ -1,6 +1,6 @@
<script>
import { createEventDispatcher } from 'svelte';
import { enter } from './events.js';
import { enter } from '../events.js';
export let examples;
export let name;

@ -1,5 +1,5 @@
<script>
import { user, logout } from '../../../user.js';
import { user, logout } from '../../../../user.js';
let showMenu = false;
let name;

@ -2,10 +2,10 @@
import { createEventDispatcher } from 'svelte';
import ExampleSelector from './ExampleSelector.html';
import UserMenu from './UserMenu.html';
import Icon from '../../../components/icon.html';
import Icon from '../../../../components/icon.html';
import * as doNotZip from 'do-not-zip';
import downloadBlob from '../_utils/downloadBlob.js';
import { user } from '../../../user.js';
import downloadBlob from '../../_utils/downloadBlob.js';
import { user } from '../../../../user.js';
const dispatch = createEventDispatcher();

@ -1,3 +1,16 @@
<script context="module">
let codemirror_promise;
let _CodeMirror;
if (process.browser) {
codemirror_promise = import(/* webpackChunkName: "codemirror" */ './_codemirror.js');
codemirror_promise.then(mod => {
_CodeMirror = mod.default;
});
}
</script>
<script>
import { onMount, beforeUpdate, createEventDispatcher } from 'svelte';
@ -9,6 +22,8 @@
export let error;
export let errorLoc;
export let warningCount = 0;
export let flex = false;
export let lineNumbers = true;
let w;
let h;
@ -29,12 +44,12 @@
};
const refs = {};
let CodeMirror;
let editor;
let updating = false;
let marker;
let error_line;
let destroyed = false;
let CodeMirror;
$: if (CodeMirror) {
createEditor(mode);
@ -79,9 +94,13 @@
}
onMount(() => {
import(/* webpackChunkName: "codemirror" */ './_codemirror.js').then(mod => {
if (_CodeMirror) {
CodeMirror = _CodeMirror;
} else {
codemirror_promise.then(mod => {
CodeMirror = mod.default;
});
}
return () => {
destroyed = true;
@ -101,7 +120,7 @@
}
editor = CodeMirror.fromTextArea(refs.editor, {
lineNumbers: true,
lineNumbers,
lineWrapping: true,
indentWithTabs: true,
indentUnit: 2,
@ -117,6 +136,7 @@
if (!updating) {
updating = true;
code = instance.getValue();
dispatch('change', { value: code });
}
});
@ -131,11 +151,20 @@
.codemirror-container :global(.CodeMirror) {
height: 100%;
background: var(--background);
/* background: var(--background); */
background: transparent;
font: 300 var(--code-fs)/1.7 var(--font-mono);
color: var(--base);
}
.codemirror-container.flex :global(.CodeMirror) {
height: auto;
}
.codemirror-container.flex :global(.CodeMirror-lines) {
padding: 0;
}
@media (min-width: 768px) {
.codemirror-container {
height: 100%;
@ -186,7 +215,7 @@
- just started to transfer colors from prism to codemirror
-----------------------------------------------
-->
<div class='codemirror-container' bind:offsetWidth={w} bind:offsetHeight={h}>
<div class='codemirror-container' class:flex bind:offsetWidth={w} bind:offsetHeight={h}>
<textarea tabindex='2' bind:this={refs.editor}></textarea>
{#if error}

@ -5,9 +5,11 @@
const dispatch = createEventDispatcher();
export let components;
export let components = [];
export let selectedComponent;
let editing = null;
// let previous_components;
// $: {
@ -20,7 +22,7 @@
function selectComponent(component, selectedComponent) {
if (selectedComponent != component) {
selectedComponent.edit = false;
editing = null;
}
dispatch('select', { component });
@ -28,7 +30,7 @@
function editTab(component, selectedComponent) {
if (selectedComponent === component) {
selectedComponent.edit = true; // TODO can we make this local state?
editing = selectedComponent;
}
}
@ -36,7 +38,7 @@
const match = /(.+)\.(html|js)$/.exec(selectedComponent.name);
selectedComponent.name = match ? match[1] : selectedComponent.name;
if (match && match[2]) selectedComponent.type = match[2];
selectedComponent.edit = false;
editing = null;
components = components; // TODO necessary?
}
@ -51,12 +53,34 @@
event.target.select();
});
}
let uid = 1;
function addNew() {
const component = {
name: uid++ ? `Component${uid}` : 'Component1',
type: 'html',
source: ''
};
editing = component;
setTimeout(() => {
// TODO we can do this without IDs
document.getElementById(component.name).scrollIntoView(false);
// selectedComponent = component;
});
dispatch('create', { component });
dispatch('select', { component });
}
</script>
<style>
.component-selector {
position: relative;
border-bottom: 1px solid #eee;
overflow: hidden;
}
.file-tabs {
@ -66,7 +90,7 @@
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
height: 100%;
height: 10em;
}
.file-tabs button {
@ -146,11 +170,11 @@
width: 5rem;
height: 100%;
text-align: center;
background-color: white;
}
.add-new:hover {
color: var(--flash);
background-color: white;
}
</style>
@ -169,8 +193,8 @@
App.html
</div>
{:else}
{#if component.edit}
<span class="input-sizer">{component.name + (/\./.test(component.name) ? '' : '.html')}</span>
{#if component === editing}
<span class="input-sizer">{component.name + (/\./.test(component.name) ? '' : `.${component.type}`)}</span>
<input
autofocus
@ -198,7 +222,7 @@
{/each}
</div>
<button class="add-new" on:click="{() => dispatch('create')}" title="add new component">
<button class="add-new" on:click="{addNew}" title="add new component">
<Icon name="plus" />
</button>
</div>

@ -1,7 +1,7 @@
<script>
import CodeMirror from '../CodeMirror.html';
export let selectedComponent;
export let component;
export let error;
export let errorLoc;
export let warningCount = 0;
@ -24,13 +24,14 @@
</style>
<div class="editor-wrapper">
{#if selectedComponent}
{#if component}
<CodeMirror
mode="{selectedComponent.type === 'js' ? 'javascript' : 'handlebars'}"
bind:code={selectedComponent.source}
mode="{component.type === 'js' ? 'javascript' : 'handlebars'}"
bind:code={component.source}
{error}
{errorLoc}
{warningCount}
on:change
on:navigate
/>
{/if}

@ -10,6 +10,7 @@
export let warningCount;
</script>
<!-- TODO would be nice if events bubbled -->
<ComponentSelector
{components}
bind:selectedComponent
@ -19,9 +20,10 @@
/>
<ModuleEditor
bind:selectedComponent
component={selectedComponent}
error={sourceError}
errorLoc="{sourceErrorLoc || runtimeErrorLoc}"
{warningCount}
on:change
on:navigate
/>

@ -0,0 +1,55 @@
<script>
import { createEventDispatcher } from 'svelte';
import * as fleece from 'golden-fleece';
import CodeMirror from '../CodeMirror.html';
const dispatch = createEventDispatcher();
export let value;
let json5;
if (value === undefined) {
json5 = 'undefined';
} else {
json5 = fleece.stringify(value);
}
$: try {
value = fleece.evaluate(json5);
} catch (err) {
console.error(Object.keys(err));
// TODO show in UI
}
function handleChange(event) {
try {
const value = fleece.evaluate(event.detail.value);
dispatch('change', { value });
} catch (err) {
// TODO indicate error
}
}
</script>
<style>
.prop-editor {
border: 1px solid #eee;
}
</style>
<div class="prop-editor">
<CodeMirror
mode="json"
bind:code={json5}
lineNumbers={false}
on:change={handleChange}
flex
/>
</div>
<!-- <CodeMirror
mode="json"
bind:code={json5}
error={dataError}
errorLoc={dataErrorLoc}
/> -->

@ -8,10 +8,19 @@
export let bundle;
export let dom;
export let ssr;
export let data;
export let values;
export let props;
export let sourceError;
export let error;
export function setProp(prop, value) {
if (component) {
component[prop] = value;
}
}
let component;
const refs = {};
const importCache = {};
let pendingImports = 0;
@ -70,8 +79,6 @@
let init;
onMount(() => {
let component;
refs.child.addEventListener('load', () => {
const iframe = refs.child;
const body = iframe.contentDocument.body;
@ -150,7 +157,7 @@
const createHtml = () => {
try {
evalInIframe(`${ssr.code}
var rendered = SvelteComponent.render(${JSON.stringify(data)});
var rendered = SvelteComponent.render(${JSON.stringify(values)});
if (rendered.css.code) {
var style = document.createElement('style');
@ -183,7 +190,7 @@
var component = new SvelteComponent({
target: document.body,
data: ${JSON.stringify(data)}
props: ${JSON.stringify(values)}
});`);
component = window.app = window.component = iframe.contentWindow.component;
@ -249,17 +256,18 @@
toDestroy = component;
component = null;
if (data !== undefined) init();
if (values !== undefined) init();
};
data_handler = data => {
values_handler = values => {
console.log({ values });
if (updating) return;
try {
if (component) {
error = null;
updating = true;
component.$set(data);
component.$set(values);
updating = false;
} else {
init();
@ -274,16 +282,35 @@
error = e;
}
};
props_handler = props => {
if (!component) {
// TODO can this happen?
console.error(`no component to bind to`);
return;
}
props.forEach(prop => {
// TODO should there be a public API for binding?
// e.g. `component.$watch(prop, handler)`?
// (answer: probably)
component.$$.bound[prop] = value => {
dispatch('binding', { prop, value });
};
});
};
});
});
function noop(){}
let run = noop;
let bundle_handler = noop;
let data_handler = noop;
let values_handler = noop;
let props_handler = noop;
$: bundle_handler(bundle);
$: data_handler(data);
$: values_handler(values);
$: props_handler(props);
</script>
<style>

@ -1,20 +1,36 @@
<script>
import SplitPane from '../SplitPane.html';
import Viewer from './Viewer.html';
import PropEditor from './PropEditor.html';
import CodeMirror from '../CodeMirror.html';
export let bundle
export let compiled;
export let dom;
export let ssr;
export let data;
export let props;
export let values;
export let json5;
export let sourceError;
export let runtimeError;
export let dataError;
export let dataErrorLoc;
// refs
let viewer;
const propEditors = {};
let view = 'result';
function setPropFromViewer(prop, value) {
// console.log(`setting prop from component`, prop, value);
// propEditors[prop].setValue(value);
}
function setPropFromEditor(prop, value) {
console.log(`setting prop from editor`, prop, value);
viewer.setProp(prop, value);
}
</script>
<style>
@ -46,6 +62,27 @@
font: 300 1.2rem/1.5 var(--font-ui);
padding: 1.2rem 0 0.8rem 1rem;
}
.loading.message {
position: absolute !important;
background-color: #666;
top: 1em;
left: 50%;
transform: translate(-50%,0);
}
.props {
display: grid;
padding: 0.5em;
grid-template-columns: auto 1fr;
grid-template-rows: min-content;
grid-column-gap: 0.5em;
overflow-y: auto;
}
.options {
padding: 0.5em;
}
</style>
<div class="view-toggle">
@ -66,31 +103,39 @@
<div slot="a">
{#if bundle}
<Viewer
bind:this={viewer}
{bundle}
{dom}
{ssr}
{data}
bind:values
{props}
{sourceError}
bind:error={runtimeError}
on:data="{e => updateData(e.detail.current)}"
on:navigate={navigate}
on:binding="{e => setPropFromViewer(e.detail.prop, e.detail.value)}"
/>
{:else}
<p class="loading">loading Svelte compiler...</p>
<!-- TODO componentise this -->
<p class="loading message">loading Svelte compiler...</p>
{/if}
</div>
<section slot="b">
<h3>
Props editor
</h3>
<h3>Props editor</h3>
<CodeMirror
mode="json"
bind:code={json5}
error={dataError}
errorLoc={dataErrorLoc}
<div class="props">
{#each props.sort() as prop (prop)}
<code style="display: block; whitespace: pre;">{prop}</code>
<!-- TODO `bind:this={propEditors[prop]}` — currently fails -->
<PropEditor
value={values[prop]}
on:change="{e => setPropFromEditor(prop, e.detail.value)}"
/>
{/each}
</div>
</section>
</SplitPane>
{:else}
@ -106,11 +151,9 @@
</div>
<section slot="b">
<h3>
Compiler options
</h3>
<h3>Compiler options</h3>
<div>TODO</div>
<div class="options">TODO</div>
</section>
</SplitPane>
{/if}

@ -9,8 +9,7 @@
export let version;
export let components;
export let selectedComponent;
export let data;
export let json5;
export let values;
let bundle = null;
let dom;
@ -20,10 +19,11 @@
let dataError = null;
let dataErrorLoc = null;
let warningCount = 0;
let compiled = '';
let code = '';
let uid = 0;
let sourceErrorLoc, runtimeErrorLoc;
let props = [];
let sourceErrorLoc;
let runtimeErrorLoc;
let worker;
@ -38,11 +38,13 @@
case 'bundled':
({ bundle, dom, ssr, warningCount, error: sourceError } = event.data.result);
console.log(dom, sourceError);
runtimeError = null;
break;
case 'compiled':
compiled = event.data.result;
code = event.data.result.code;
if (event.data.result.props) props = event.data.result.props;
break;
}
}
@ -57,22 +59,6 @@
};
});
function createComponent() {
const newComponent = {
name: uid++ ? `Component${uid}` : 'Component1',
type: 'html',
source: '',
edit: true
};
components = components.concat(newComponent);
setTimeout(() => {
document.getElementById(newComponent.name).scrollIntoView(false);
selectedComponent = newComponent;
});
}
function removeComponent() {
const component = selectedComponent;
@ -92,6 +78,35 @@
}
}
function compile(component) {
if (component.type === 'html') {
worker.postMessage({
type: 'compile',
component,
entry: component === components[0]
});
} else {
code = component.source;
}
}
function handleSelect(event) {
console.log(`handleSelect`);
selectedComponent = event.detail.component;
// compile(selectedComponent);
}
function handleChange() {
console.log(`handleChange`);
// recompile selected component
compile(selectedComponent);
// regenerate bundle (TODO do this in a separate worker?)
worker.postMessage({ type: 'bundle', components });
}
function updateData(current) {
const data = fleece.evaluate(json5);
@ -121,6 +136,15 @@
: null;
}
$: if (worker && components) {
console.log(`posting`, worker, components);
worker.postMessage({ type: 'bundle', components });
}
$: if (worker && selectedComponent) {
compile(selectedComponent);
}
$: try {
data= fleece.evaluate(json5);
dataError = null;
@ -129,34 +153,6 @@
dataError = err;
dataErrorLoc = err && err.loc;
}
$: if (worker && components.length > 0) {
worker.postMessage({ type: 'bundle', components });
}
let last_selected_component;
$: {
// slightly counterintuitively, we only want to rebundle if
// this is the *same* component — not if we've just selected
// a different one
// if (selectedComponent === last_selected_component) {
// if (worker && components.length > 0) {
// console.log(`2`, components);
// worker.postMessage({ type: 'bundle', components });
// }
// }
// recompile the currently selected component
if (selectedComponent) {
if (selectedComponent.type === 'html') {
worker.postMessage({ type: 'compile', component: selectedComponent });
} else {
compiled = selectedComponent.source;
}
}
last_selected_component = selectedComponent;
}
</script>
<style>
@ -243,25 +239,26 @@
<SplitPane type="horizontal">
<section slot=a>
<Input
{components}
bind:selectedComponent
bind:components
{selectedComponent}
error={sourceError}
errorLoc="{sourceErrorLoc || runtimeErrorLoc}"
{warningCount}
on:create={createComponent}
on:remove={removeComponent}
on:create="{e => components = components.concat(e.detail.component)}"
on:select="{e => selectedComponent = e.detail.component}"
on:remove={removeComponent}
on:change="{handleChange}"
/>
</section>
<section slot=b style='height: 100%;'>
<Output
{compiled}
{code}
{bundle}
{ssr}
{dom}
{data}
{json5}
{props}
{values}
{sourceError}
{runtimeError}
{dataError}

@ -1,62 +0,0 @@
.module-name {
position: relative;
display: block;
background-color: #ff00d4;
}
.panel-header {
/* padding: 0 40px .5em 0; */
background-color: #ff00d4;
}
.dropdown {
position: relative;
display: block;
float: left;
padding: 0 2em 0 0;
background-color: #ff00d4;
}
.dropdown::after {
content: '▼';
position: absolute;
right: 1rem;
top: .55rem;
font-size: .8em;
color: #999;
pointer-events: none;
background-color: #ff00d4;
}
.input-wrapper {
position: relative;
display: block;
float: left;
line-height: 1;
/* margin: 0 .3em 0 0; */
background-color: #ff00d4;
}
.file-tabs li.active {
/* background-color: var(--back-light); */
background-color: #ff00d4;
}
.widther {
display: block;
font-family: inherit;
font-size: inherit;
border: 1px solid #eee;
padding: calc(.5em - 1px) .25em;
line-height: 1;
background-color: #ff00d4;
}
.file-extension {
display: inline-block;
padding: calc(.5em - 1px) 0;
color: var(--prime);
left: -.2em;
pointer-events: none;
background-color: #ff00d4;
}

@ -1,45 +1,25 @@
<script context="module">
// export async function preload({ query }) {
// return {
// examples,
// version: query.version || 'latest',
// components: [],
// data: {},
// name: 'loading...',
// json5: '{}',
// gist: null
// };
// }
</script>
<script>
import { onMount } from 'svelte';
import { locate } from 'locate-character';
import * as fleece from 'golden-fleece';
import AppControls from './_components/AppControls.html';
import AppControls from './_components/AppControls/index.html';
import Repl from './_components/Repl.html';
import examples from '../../../content/examples/manifest.json';
export let query;
const refs = {};
let version = query.version || 'alpha';
let components = [];
let components;
let selectedComponent;
let data = {};
let values = {};
let name = 'loading...';
let json5 = '{}';
let gist = null;
let zen_mode = false;
let bundle;
$: if (typeof history !== 'undefined') {
const params = [];
if (version !== 'latest') params.push(`version=${version}`);
if (query.gist) params.push(`gist=${gist.id}`);
else if (query.data) params.push(`data=${query.data}`);
if (version !== 'latest') params.push(`version=${version}`);
if (query.gist) params.push(`gist=${query.gist}`);
else if (query.demo) params.push(`demo=${query.demo}`);
const url = params.length > 0
@ -56,33 +36,11 @@
});
});
function tryParseData(encoded) {
try {
const base64 = decodeURIComponent(encoded);
const json = atob(base64);
return JSON.parse(json);
} catch (err) {
console.error(err.message);
return {};
}
}
function stringifyComponents(components) {
return JSON.stringify(
components.map(component => {
return {
name: component.name,
source: component.source
};
})
);
}
onMount(() => {
if (query.gist) {
fetch(`gist/${query.gist}`).then(r => r.json()).then(gist => {
name = gist.description;
json5 = '{}';
values = {};
components = Object.keys(gist.files)
.map(file => {
@ -92,7 +50,9 @@
const source = gist.files[file].content;
// while we're here...
if (file === 'data.json' || file === 'data.json5') json5 = source;
if (file === 'data.json' || file === 'data.json5') {
values = tryParseData(source) || {};
}
return {
name: file.slice(0, dot),
@ -118,18 +78,31 @@
});
function load_demo(slug) {
fetch(`api/${slugs.has(query.demo) ? 'examples' : 'guide/demo'}/${query.demo}`).then(async response => {
const url = slugs.has(query.demo)
? `api/examples/${query.demo}`
: `guide/demo/${query.demo}.json`;
fetch(url).then(async response => {
if (response.ok) {
const demo = await response.json();
name = demo.title;
json5 = demo.json5 || '{}';
values = tryParseData(demo.json5) || {}; // TODO make this more error-resistant
components = demo.components;
selectedComponent = components[0];
}
});
}
function tryParseData(json5) {
try {
return fleece.evaluate(json5);
} catch (err) {
return null;
}
}
$: if (process.browser && query.demo) {
load_demo(query.demo);
}
@ -198,22 +171,19 @@
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}">
<AppControls
{examples}
{bundle}
{components}
{data}
{json5}
{gist}
bind:name
{values}
{name}
gist_id={query.gist}
bind:zen_mode
on:select="{e => (query.demo = e.detail.slug, gist = null)}"
on:forked="{e => gist = e.detail.gist}"
on:select="{e => query = Object.assign({}, query, { demo: e.detail.slug, gist: null })}"
on:forked="{e => query = Object.assign({}, query, { demo: null, gist: e.detail.gist })}"
/>
<Repl
bind:version
bind:selectedComponent
{components}
{data}
{json5}
{selectedComponent}
{values}
/>
</div>

@ -0,0 +1,15 @@
export default function process_markdown(markdown) {
const match = /---\n([\s\S]+?)\n---/.exec(markdown);
const frontMatter = match[1];
const content = markdown.slice(match[0].length);
const metadata = {};
frontMatter.split('\n').forEach(pair => {
const colonIndex = pair.indexOf(':');
metadata[pair.slice(0, colonIndex).trim()] = pair
.slice(colonIndex + 1)
.trim();
});
return { metadata, content };
}

@ -339,7 +339,6 @@ a:hover > .icon { stroke: var(--flash) }
/* tables --------------------------------- */
table {
margin: 4rem 0;
width: 100%;
font-size: var(--h5);
}

@ -1,10 +1,10 @@
self.window = self; // egregious hack to get magic-string to work in a worker
let ready = false;
let pending_components;
let pending_component;
let version;
let fulfil_ready;
const ready = new Promise(f => {
fulfil_ready = f;
});
self.addEventListener('message', async event => {
switch (event.data.type) {
@ -17,30 +17,21 @@ self.addEventListener('message', async event => {
break;
case 'bundle':
if (ready) {
const result = await bundle(event.data.components);
if (result) {
await ready;
postMessage({
type: 'bundled',
result
result: await bundle(event.data.components)
});
}
} else {
pending_components = event.data.components;
}
break;
case 'compile':
if (ready) {
const result = await compile(event.data.component);
await ready;
postMessage({
type: 'compiled',
result
result: compile(event.data.component, event.data.entry)
});
} else {
pending_component = event.data.component;
}
break;
}
});
@ -59,29 +50,7 @@ async function init(version) {
`https://unpkg.com/rollup/dist/rollup.browser.js`
);
if (pending_components) {
const result = await bundle(pending_components);
if (result) {
postMessage({
type: 'bundled',
result
});
}
pending_components = null;
}
if (pending_component) {
const result = await compile(pending_component);
postMessage({
type: 'compiled',
result
});
pending_component = null;
}
ready = true;
fulfil_ready();
return version === 'local' ? version : svelte.VERSION;
}
@ -264,19 +233,19 @@ async function bundle(components) {
}
}
function compile(component) {
function compile(component, entry) {
try {
const { js } = svelte.compile(component.source, Object.assign({
const { js, stats } = svelte.compile(component.source, Object.assign({
// TODO make options configurable
name: component.name,
filename: component.name + '.html',
}, commonCompilerOptions));
return js.code;
return { code: js.code, props: entry ? stats.props : null };
} catch (err) {
let result = `/* Error compiling component\n\n${err.message}`;
if (err.frame) result += `\n${err.frame}`;
result += `\n\n*/`;
return result;
return { code: result, props: null };
}
}

Loading…
Cancel
Save