Redesign site and streamline playground
This commit is contained in:
@@ -2,7 +2,20 @@
|
|||||||
import { nav } from '../lib/docs.js';
|
import { nav } from '../lib/docs.js';
|
||||||
import '../style.css';
|
import '../style.css';
|
||||||
|
|
||||||
const { title = 'Decodal', active = '', playground = false, frontpage = false } = Astro.props;
|
const {
|
||||||
|
title = 'Decodal',
|
||||||
|
description = 'Decodal is a deterministic data language for configuration, constraints, and defaults.',
|
||||||
|
active = '',
|
||||||
|
playground = false,
|
||||||
|
frontpage = false,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const pathname = Astro.url.pathname;
|
||||||
|
const language = active ? 'ja' : 'en';
|
||||||
|
|
||||||
|
function isCurrent(path) {
|
||||||
|
return pathname === path || (path !== '/' && pathname.startsWith(path));
|
||||||
|
}
|
||||||
|
|
||||||
function renderNav(items, prefix = []) {
|
function renderNav(items, prefix = []) {
|
||||||
return `<ul class="nav-tree">${items
|
return `<ul class="nav-tree">${items
|
||||||
@@ -17,22 +30,26 @@ function renderNav(items, prefix = []) {
|
|||||||
}
|
}
|
||||||
---
|
---
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang={language}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content={description} />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<a class="brand" href="/">Decodal</a>
|
<a class="brand" href="/" aria-label="Decodal home">
|
||||||
<nav class="topnav">
|
<strong>Decodal</strong>
|
||||||
<a href="/docs/introduction/">Docs</a>
|
<span>Deferred Constraint Data Language</span>
|
||||||
<a href="/playground/">Playground</a>
|
</a>
|
||||||
|
<nav class="topnav" aria-label="Primary navigation">
|
||||||
|
<a class:list={{ current: isCurrent('/docs/') }} aria-current={isCurrent('/docs/') ? 'page' : undefined} href="/docs/introduction/">Manual</a>
|
||||||
|
<a class:list={{ current: isCurrent('/playground/') }} aria-current={isCurrent('/playground/') ? 'page' : undefined} href="/playground/">Playground</a>
|
||||||
<a href="https://gitea.hareworks.net/Hare/Decodal">Source</a>
|
<a href="https://gitea.hareworks.net/Hare/Decodal">Source</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<div class={playground || frontpage ? 'layout playground-layout' : 'layout'}>
|
<div class:list={{ layout: true, 'wide-layout': playground || frontpage, 'playground-layout': playground }}>
|
||||||
{!playground && !frontpage && (
|
{!playground && !frontpage && (
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
@@ -45,16 +62,20 @@ function renderNav(items, prefix = []) {
|
|||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<footer class="site-footer">
|
{!playground && (
|
||||||
<span>2026 Hare</span>
|
<footer class="site-footer">
|
||||||
<a href="https://gitea.hareworks.net/Hare/Decodal">Source</a>
|
<span>Decodal</span>
|
||||||
</footer>
|
<span aria-hidden="true">·</span>
|
||||||
|
<span>MIT or Apache-2.0</span>
|
||||||
|
<a href="https://gitea.hareworks.net/Hare/Decodal">Source</a>
|
||||||
|
</footer>
|
||||||
|
)}
|
||||||
<script>
|
<script>
|
||||||
const sidebar = document.querySelector('.sidebar-content');
|
const sidebar = document.querySelector('.sidebar-content');
|
||||||
|
|
||||||
if (sidebar instanceof HTMLElement) {
|
if (sidebar instanceof HTMLElement) {
|
||||||
const updateStickyOffset = () => {
|
const updateStickyOffset = () => {
|
||||||
const stickyTop = Math.min(0, window.innerHeight - sidebar.offsetHeight);
|
const stickyTop = Math.min(16, window.innerHeight - sidebar.offsetHeight - 16);
|
||||||
sidebar.style.setProperty('--sidebar-sticky-top', `${stickyTop}px`);
|
sidebar.style.setProperty('--sidebar-sticky-top', `${stickyTop}px`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,17 @@ export function allDocSlugs() {
|
|||||||
return Object.keys(docs).filter((slug) => slug !== 'index');
|
return Object.keys(docs).filter((slug) => slug !== 'index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function docTitle(slug, items = nav) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.slug === slug) return item.title;
|
||||||
|
if (item.children) {
|
||||||
|
const childTitle = docTitle(slug, item.children);
|
||||||
|
if (childTitle) return childTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderMarkdown(slug) {
|
export function renderMarkdown(slug) {
|
||||||
const source = docs[slug] ?? docs.index;
|
const source = docs[slug] ?? docs.index;
|
||||||
const html = marked.parse(source ?? '# Not found\n');
|
const html = marked.parse(source ?? '# Not found\n');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const DECODAL_KEYWORDS = new Set(['let', 'in', 'fn', 'match', 'import', 'default']);
|
const DECODAL_KEYWORDS = new Set(['let', 'in', 'fn', 'match', 'import', 'as', 'default']);
|
||||||
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool', 'Unknown']);
|
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool', 'Unknown']);
|
||||||
const DECODAL_LITERALS = new Set(['true', 'false']);
|
const DECODAL_LITERALS = new Set(['true', 'false']);
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export function highlightDecodalTokens(source, tokens) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tokenClass(kind, text) {
|
function tokenClass(kind, text) {
|
||||||
if (['let', 'in', 'fn', 'match', 'import', 'default'].includes(kind)) return 'tok-keyword';
|
if (['let', 'in', 'fn', 'match', 'import', 'as', 'default'].includes(kind)) return 'tok-keyword';
|
||||||
if (kind === 'ident' && ['String', 'Int', 'Float', 'Bool', 'Unknown'].includes(text)) return 'tok-type';
|
if (kind === 'ident' && ['String', 'Int', 'Float', 'Bool', 'Unknown'].includes(text)) return 'tok-type';
|
||||||
if (kind === 'ident') return '';
|
if (kind === 'ident') return '';
|
||||||
if (['true', 'false'].includes(kind)) return 'tok-literal';
|
if (['true', 'false'].includes(kind)) return 'tok-literal';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import ManualLayout from '../../layouts/ManualLayout.astro';
|
import ManualLayout from '../../layouts/ManualLayout.astro';
|
||||||
import { allDocSlugs, renderMarkdown } from '../../lib/docs.js';
|
import { allDocSlugs, docTitle, renderMarkdown } from '../../lib/docs.js';
|
||||||
|
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
return allDocSlugs().map((slug) => ({
|
return allDocSlugs().map((slug) => ({
|
||||||
@@ -11,7 +11,8 @@ export function getStaticPaths() {
|
|||||||
|
|
||||||
const { slug } = Astro.props;
|
const { slug } = Astro.props;
|
||||||
const html = renderMarkdown(slug);
|
const html = renderMarkdown(slug);
|
||||||
|
const title = docTitle(slug);
|
||||||
---
|
---
|
||||||
<ManualLayout title={`Decodal - ${slug}`} active={slug}>
|
<ManualLayout title={`${title} — Decodal Manual`} active={slug}>
|
||||||
<article class="markdown" set:html={html} />
|
<article class="markdown" set:html={html} />
|
||||||
</ManualLayout>
|
</ManualLayout>
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import { renderMarkdown } from '../../lib/docs.js';
|
|||||||
|
|
||||||
const html = renderMarkdown('index');
|
const html = renderMarkdown('index');
|
||||||
---
|
---
|
||||||
<ManualLayout title="Decodal Manual" active="index">
|
<ManualLayout title="Decodal Manual" description="Decodalの言語仕様と、アプリケーションへ組み込むための公開API。" active="index">
|
||||||
<article class="markdown" set:html={html} />
|
<article class="markdown" set:html={html} />
|
||||||
</ManualLayout>
|
</ManualLayout>
|
||||||
|
|||||||
@@ -1,75 +1,163 @@
|
|||||||
---
|
---
|
||||||
import ManualLayout from '../layouts/ManualLayout.astro';
|
import ManualLayout from '../layouts/ManualLayout.astro';
|
||||||
|
import { highlightCode } from '../lib/highlight.js';
|
||||||
|
|
||||||
const example = `Port = Int >= 1 & <= 65535 default 8080;
|
const example = `let
|
||||||
|
Port = Int & >= 1 & <= 65535;
|
||||||
|
Service = {
|
||||||
|
host = String;
|
||||||
|
port = Port default 8080;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
host = "127.0.0.1";
|
||||||
|
} as Service;`;
|
||||||
|
|
||||||
Server = {
|
const result = `{
|
||||||
host = String default "localhost";
|
"host": "127.0.0.1",
|
||||||
port = Port;
|
"port": 8080
|
||||||
};
|
}`;
|
||||||
|
|
||||||
Production = Server & {
|
|
||||||
host = "example.com";
|
|
||||||
};`;
|
|
||||||
---
|
---
|
||||||
<ManualLayout title="Decodal" frontpage>
|
<ManualLayout
|
||||||
<section class="hero">
|
title="Decodal — Deferred Constraint Data Language"
|
||||||
<h1>Decodal keeps configuration, constraints, and defaults in one composable value.</h1>
|
description="Decodal is a deterministic data language for describing, composing, validating, and materializing structured data."
|
||||||
<p class="hero-copy">
|
frontpage
|
||||||
Define structured data with constraints, compose it with patches, and materialize the result from Rust,
|
>
|
||||||
WebAssembly, or the browser playground.
|
<section class="home-intro" aria-labelledby="home-title">
|
||||||
</p>
|
<div class="home-intro-copy">
|
||||||
<div class="hero-actions">
|
<h1 id="home-title">Configuration, constraints, and defaults are values.</h1>
|
||||||
<a class="button primary" href="/playground/">Try the playground</a>
|
<p class="home-lead">
|
||||||
<a class="button secondary" href="/docs/introduction/">Read the manual</a>
|
Decodal is a small deterministic language for describing, composing, validating, and materializing
|
||||||
|
structured data. The same expression can carry concrete data and the range that data must satisfy.
|
||||||
|
</p>
|
||||||
|
<div class="home-actions">
|
||||||
|
<a class="button primary" href="/playground/">Open the playground</a>
|
||||||
|
<a class="text-action" href="/docs/introduction/">Read the introduction <span aria-hidden="true">→</span></a>
|
||||||
|
</div>
|
||||||
|
<dl class="home-facts">
|
||||||
|
<div>
|
||||||
|
<dt>Source</dt>
|
||||||
|
<dd><code>.dcdl</code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Runtime</dt>
|
||||||
|
<dd>Rust · WebAssembly</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Output</dt>
|
||||||
|
<dd>Validated structured data</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-code" aria-label="Decodal example and materialized result">
|
||||||
|
<div class="code-caption">
|
||||||
|
<span>service.dcdl</span>
|
||||||
|
<span>DCDL</span>
|
||||||
|
</div>
|
||||||
|
<pre class="code-block"><code class="language-dcdl" set:html={highlightCode(example, 'dcdl')} /></pre>
|
||||||
|
<div class="materialized-result">
|
||||||
|
<span>materialize</span>
|
||||||
|
<pre><code>{result}</code></pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="home-packages" aria-label="Official packages">
|
<section class="home-section" aria-labelledby="model-title">
|
||||||
<h2>Official packages</h2>
|
<header class="section-heading">
|
||||||
<a class="primary-package" href="https://crates.io/crates/decodal">
|
<h2 id="model-title">One expression system, from abstract range to concrete data.</h2>
|
||||||
<span>Rust crate</span>
|
</header>
|
||||||
<strong>crates.io/crates/decodal</strong>
|
<div class="definition-list">
|
||||||
</a>
|
<article>
|
||||||
<ul class="package-list">
|
<code>&</code>
|
||||||
<li>decodal-wasm on <a href="https://jsr.io/@hare/decodal-wasm@0.3.0">jsr</a> / <a href="https://www.npmjs.com/package/decodal-wasm">npm</a></li>
|
<div>
|
||||||
<li>decodal-codemirror on <a href="https://jsr.io/@hare/decodal-codemirror@0.3.0">jsr</a> / <a href="https://www.npmjs.com/package/decodal-codemirror">npm</a></li>
|
<h3>Compose without discarding constraints</h3>
|
||||||
</ul>
|
<p>Symmetric composition keeps information from both sides and fails when their ranges cannot overlap.</p>
|
||||||
</section>
|
</div>
|
||||||
|
</article>
|
||||||
<section class="home-grid" aria-label="Decodal overview">
|
<article>
|
||||||
<article class="home-card">
|
<code>as</code>
|
||||||
<h2>What it is</h2>
|
<div>
|
||||||
<p>
|
<h3>Verify that a value is more specific</h3>
|
||||||
Decodal is a small data language for configuration shapes that need validation, defaults,
|
<p>The left side must fit inside the wider range on the right. Wider abstract fields remain available for later materialization.</p>
|
||||||
lazy imports, and predictable composition.
|
</div>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
<article>
|
||||||
<article class="home-card">
|
<code>//</code>
|
||||||
<h2>Where it runs</h2>
|
<div>
|
||||||
<p>
|
<h3>Apply an explicit structural patch</h3>
|
||||||
Use the Rust crate for runtime embedding, the WASM package in browsers, CodeMirror/Lezer for Web editing
|
<p>Right-biased composition replaces the selected structure while preserving unrelated fields.</p>
|
||||||
and formatting, and Tree-sitter for general editor integration.
|
</div>
|
||||||
</p>
|
</article>
|
||||||
</article>
|
<article>
|
||||||
<article class="home-card">
|
<code>default</code>
|
||||||
<h2>Start here</h2>
|
<div>
|
||||||
<p>
|
<h3>Choose a fallback only when materializing</h3>
|
||||||
Open the playground to run examples in your browser, or read the components page to see how the runtime and
|
<p>A default does not erase the range. It is selected only when no more concrete value has been supplied.</p>
|
||||||
editor pieces fit together.
|
</div>
|
||||||
</p>
|
</article>
|
||||||
<p><a href="/docs/components/">View components →</a></p>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="home-example" aria-label="Decodal example">
|
|
||||||
<div>
|
|
||||||
<h2>A compact example</h2>
|
|
||||||
<p>
|
|
||||||
Constraints and defaults are values. Compose them with concrete data and materialize when the host knows what
|
|
||||||
output shape it expects.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<pre><code>{example}</code></pre>
|
</section>
|
||||||
|
|
||||||
|
<section class="home-section host-section" aria-labelledby="host-title">
|
||||||
|
<header class="section-heading">
|
||||||
|
<h2 id="host-title">The host owns I/O. Decodal owns evaluation.</h2>
|
||||||
|
<p>
|
||||||
|
Filesystems, Markdown, JSON, network resources, and application globals stay under host control. Decodal
|
||||||
|
receives source or structured values and evaluates them with the same rules in production and editor tooling.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<ol class="evaluation-flow" aria-label="Evaluation flow">
|
||||||
|
<li>
|
||||||
|
<span>1</span>
|
||||||
|
<strong>Host environment</strong>
|
||||||
|
<small>globals · source · structured imports</small>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>2</span>
|
||||||
|
<strong>Evaluate</strong>
|
||||||
|
<small>lazy references · constraints · composition</small>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>3</span>
|
||||||
|
<strong>Materialize</strong>
|
||||||
|
<small>defaults · validation · diagnostics</small>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>4</span>
|
||||||
|
<strong>Application data</strong>
|
||||||
|
<small>Data · Rust type · JavaScript value</small>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p class="section-link"><a href="/docs/embedding/">Read the embedding guide <span aria-hidden="true">→</span></a></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-section package-section" aria-labelledby="packages-title">
|
||||||
|
<header class="section-heading">
|
||||||
|
<h2 id="packages-title">Use only the layer your application needs.</h2>
|
||||||
|
</header>
|
||||||
|
<div class="package-rows">
|
||||||
|
<article>
|
||||||
|
<div><code>decodal</code><span>Rust</span></div>
|
||||||
|
<p>Runtime, host embedding, schema derivation, and typed decoding.</p>
|
||||||
|
<a href="https://crates.io/crates/decodal">crates.io</a>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<div><code>decodal-wasm</code><span>JavaScript</span></div>
|
||||||
|
<p>Browser runtime and host-configurable semantic language service.</p>
|
||||||
|
<span class="package-links"><a href="https://jsr.io/@hare/decodal-wasm@0.3.0">JSR</a><a href="https://www.npmjs.com/package/decodal-wasm">npm</a></span>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<div><code>decodal-lsp</code><span>Rust</span></div>
|
||||||
|
<p>Language Server Protocol with application-defined environments.</p>
|
||||||
|
<a href="https://crates.io/crates/decodal-lsp">crates.io</a>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<div><code>decodal-codemirror</code><span>JavaScript</span></div>
|
||||||
|
<p>CodeMirror 6 syntax, indentation, folding, highlighting, and formatter integration.</p>
|
||||||
|
<span class="package-links"><a href="https://jsr.io/@hare/decodal-codemirror@0.3.0">JSR</a><a href="https://www.npmjs.com/package/decodal-codemirror">npm</a></span>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<p class="section-link"><a href="/docs/components/">Compare packages and integrations <span aria-hidden="true">→</span></a></p>
|
||||||
</section>
|
</section>
|
||||||
</ManualLayout>
|
</ManualLayout>
|
||||||
|
|||||||
@@ -4,27 +4,23 @@ import ManualLayout from '../layouts/ManualLayout.astro';
|
|||||||
<ManualLayout title="Decodal Playground" playground>
|
<ManualLayout title="Decodal Playground" playground>
|
||||||
<section class="playground-page">
|
<section class="playground-page">
|
||||||
<div class="playground-header">
|
<div class="playground-header">
|
||||||
<div>
|
|
||||||
<h1>Playground</h1>
|
|
||||||
</div>
|
|
||||||
<div class="playground-actions">
|
<div class="playground-actions">
|
||||||
<div class="action-group example-actions">
|
<div class="action-group example-actions">
|
||||||
<label class="example-picker">
|
<label class="example-picker">
|
||||||
<span>Example</span>
|
<span>Example</span>
|
||||||
<select id="example-select" aria-label="Playground example"></select>
|
<select id="example-select" aria-label="Playground example"></select>
|
||||||
</label>
|
</label>
|
||||||
<button id="load-example" type="button">Load</button>
|
<button id="load-example" class="secondary-button" type="button">Load</button>
|
||||||
</div>
|
</div>
|
||||||
<span class="action-separator" aria-hidden="true">|</span>
|
|
||||||
<div class="action-group run-actions">
|
<div class="action-group run-actions">
|
||||||
<button id="format" disabled>Format</button>
|
<button id="format" class="secondary-button" type="button" disabled>Format</button>
|
||||||
<label class="entry-picker">
|
<label class="entry-picker">
|
||||||
<span>Entry</span>
|
<span>Entry</span>
|
||||||
<select id="entry-select" aria-label="Entry point file"></select>
|
<select id="entry-select" aria-label="Entry point file"></select>
|
||||||
</label>
|
</label>
|
||||||
<button id="run" disabled>Run</button>
|
<button id="run" type="button" disabled>Run</button>
|
||||||
</div>
|
</div>
|
||||||
<p id="status" class="status">Loading WASM...</p>
|
<p id="status" class="status" aria-live="polite">Loading WASM...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -32,10 +28,20 @@ import ManualLayout from '../layouts/ManualLayout.astro';
|
|||||||
<aside class="file-panel">
|
<aside class="file-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span>Files</span>
|
<span>Files</span>
|
||||||
<button id="new-file" type="button">New</button>
|
<form id="new-file-form" class="new-file-control">
|
||||||
|
<button id="new-file" type="button" aria-label="New file" title="New file"></button>
|
||||||
|
<input
|
||||||
|
id="new-file-path"
|
||||||
|
class="new-file-input"
|
||||||
|
type="text"
|
||||||
|
aria-label="New file path"
|
||||||
|
placeholder="path/to/file.dcdl"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="file-tree" class="file-tree"></div>
|
<div id="file-tree" class="file-tree"></div>
|
||||||
<button id="delete-file" class="danger-button" type="button">Delete file</button>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="pane input-pane">
|
<section class="pane input-pane">
|
||||||
@@ -44,7 +50,7 @@ import ManualLayout from '../layouts/ManualLayout.astro';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pane output-pane">
|
<section class="pane output-pane">
|
||||||
<span>Output</span>
|
<span>Materialized output</span>
|
||||||
<pre id="output"></pre>
|
<pre id="output"></pre>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const status = document.getElementById('status');
|
|||||||
const fileTree = document.getElementById('file-tree');
|
const fileTree = document.getElementById('file-tree');
|
||||||
const activeFile = document.getElementById('active-file');
|
const activeFile = document.getElementById('active-file');
|
||||||
const newFile = document.getElementById('new-file');
|
const newFile = document.getElementById('new-file');
|
||||||
const deleteFile = document.getElementById('delete-file');
|
const newFileForm = document.getElementById('new-file-form');
|
||||||
|
const newFilePath = document.getElementById('new-file-path');
|
||||||
const exampleSelect = document.getElementById('example-select');
|
const exampleSelect = document.getElementById('example-select');
|
||||||
const entrySelect = document.getElementById('entry-select');
|
const entrySelect = document.getElementById('entry-select');
|
||||||
const loadExample = document.getElementById('load-example');
|
const loadExample = document.getElementById('load-example');
|
||||||
@@ -30,51 +31,51 @@ let languageService;
|
|||||||
|
|
||||||
const editorTheme = EditorView.theme({
|
const editorTheme = EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
backgroundColor: '#0f172a',
|
backgroundColor: '#202124',
|
||||||
color: '#e5e7eb',
|
color: '#f1f3f4',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
},
|
},
|
||||||
'.cm-scroller': {
|
'.cm-scroller': {
|
||||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
||||||
fontSize: '14px',
|
fontSize: '13px',
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.45',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
},
|
},
|
||||||
'.cm-content': {
|
'.cm-content': {
|
||||||
caretColor: '#e5e7eb',
|
caretColor: '#f1f3f4',
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
padding: '12px',
|
padding: '8px',
|
||||||
},
|
},
|
||||||
'.cm-gutters': {
|
'.cm-gutters': {
|
||||||
backgroundColor: '#111827',
|
backgroundColor: '#292a2d',
|
||||||
borderRight: '1px solid #334155',
|
borderRight: '1px solid #3c4043',
|
||||||
color: '#94a3b8',
|
color: '#9aa0a6',
|
||||||
},
|
},
|
||||||
'&.cm-focused': {
|
'&.cm-focused': {
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
},
|
},
|
||||||
'&.cm-focused .cm-cursor': {
|
'&.cm-focused .cm-cursor': {
|
||||||
borderLeftColor: '#e5e7eb',
|
borderLeftColor: '#f1f3f4',
|
||||||
},
|
},
|
||||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
||||||
backgroundColor: 'rgb(59 130 246 / 0.35)',
|
backgroundColor: '#3c5f8a',
|
||||||
},
|
},
|
||||||
'.cm-tooltip-autocomplete': {
|
'.cm-tooltip-autocomplete': {
|
||||||
backgroundColor: '#111827',
|
backgroundColor: '#292a2d',
|
||||||
border: '1px solid #334155',
|
border: '1px solid #5f6368',
|
||||||
borderRadius: '8px',
|
borderRadius: '4px',
|
||||||
color: '#e5e7eb',
|
color: '#f1f3f4',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
'.cm-tooltip-autocomplete > ul > li': {
|
'.cm-tooltip-autocomplete > ul > li': {
|
||||||
padding: '3px 8px',
|
padding: '3px 8px',
|
||||||
},
|
},
|
||||||
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {
|
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {
|
||||||
backgroundColor: '#1d4ed8',
|
backgroundColor: '#174ea6',
|
||||||
color: '#fff',
|
color: '#ffffff',
|
||||||
},
|
},
|
||||||
'.cm-completionDetail': {
|
'.cm-completionDetail': {
|
||||||
color: '#94a3b8',
|
color: '#9aa0a6',
|
||||||
fontStyle: 'normal',
|
fontStyle: 'normal',
|
||||||
marginLeft: '1.5em',
|
marginLeft: '1.5em',
|
||||||
},
|
},
|
||||||
@@ -215,7 +216,6 @@ function setActiveFile(path) {
|
|||||||
project.activePath = normalized;
|
project.activePath = normalized;
|
||||||
setEditorText(project.files[normalized]);
|
setEditorText(project.files[normalized]);
|
||||||
activeFile.textContent = normalized;
|
activeFile.textContent = normalized;
|
||||||
deleteFile.disabled = Object.keys(project.files).length <= 1;
|
|
||||||
renderFileTree();
|
renderFileTree();
|
||||||
updateRunLabel();
|
updateRunLabel();
|
||||||
saveProject();
|
saveProject();
|
||||||
@@ -276,6 +276,32 @@ function renderFileTree() {
|
|||||||
fileTree.replaceChildren(renderTreeList(tree.children));
|
fileTree.replaceChildren(renderTreeList(tree.children));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteProjectFile(path) {
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
const paths = Object.keys(project.files).sort();
|
||||||
|
if (project.files[normalized] === undefined || paths.length <= 1) return;
|
||||||
|
if (!confirm(`Delete ${normalized}?`)) return;
|
||||||
|
|
||||||
|
const deletedIndex = paths.indexOf(normalized);
|
||||||
|
const wasActive = normalized === project.activePath;
|
||||||
|
delete project.files[normalized];
|
||||||
|
|
||||||
|
const remainingPaths = Object.keys(project.files).sort();
|
||||||
|
const nextPath = remainingPaths[Math.min(deletedIndex, remainingPaths.length - 1)];
|
||||||
|
if (project.files[project.entryPath] === undefined) {
|
||||||
|
project.entryPath = wasActive ? nextPath : project.activePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasActive) {
|
||||||
|
setActiveFile(nextPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFileTree();
|
||||||
|
updateRunLabel();
|
||||||
|
saveProject();
|
||||||
|
}
|
||||||
|
|
||||||
function buildTree(paths) {
|
function buildTree(paths) {
|
||||||
const root = { name: '', children: new Map(), path: '' };
|
const root = { name: '', children: new Map(), path: '' };
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
@@ -298,6 +324,7 @@ function renderTreeList(children) {
|
|||||||
for (const child of [...children.values()].sort(compareNodes)) {
|
for (const child of [...children.values()].sort(compareNodes)) {
|
||||||
const item = document.createElement('li');
|
const item = document.createElement('li');
|
||||||
if (child.file) {
|
if (child.file) {
|
||||||
|
item.className = 'file-row';
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
button.className = child.path === project.activePath ? 'file active' : 'file';
|
button.className = child.path === project.activePath ? 'file active' : 'file';
|
||||||
@@ -305,6 +332,16 @@ function renderTreeList(children) {
|
|||||||
button.title = child.path;
|
button.title = child.path;
|
||||||
button.addEventListener('click', () => setActiveFile(child.path));
|
button.addEventListener('click', () => setActiveFile(child.path));
|
||||||
item.append(button);
|
item.append(button);
|
||||||
|
|
||||||
|
if (Object.keys(project.files).length > 1) {
|
||||||
|
const deleteButton = document.createElement('button');
|
||||||
|
deleteButton.type = 'button';
|
||||||
|
deleteButton.className = 'file-delete';
|
||||||
|
deleteButton.title = `Delete ${child.path}`;
|
||||||
|
deleteButton.setAttribute('aria-label', `Delete ${child.path}`);
|
||||||
|
deleteButton.addEventListener('click', () => deleteProjectFile(child.path));
|
||||||
|
item.append(deleteButton);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.className = 'folder';
|
label.className = 'folder';
|
||||||
@@ -322,7 +359,10 @@ function compareNodes(a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([initRuntime(runtimeWasmUrl), initDecodalFormatter(toolsWasmUrl)]);
|
await Promise.all([
|
||||||
|
initRuntime({ module_or_path: runtimeWasmUrl }),
|
||||||
|
initDecodalFormatter({ module_or_path: toolsWasmUrl }),
|
||||||
|
]);
|
||||||
languageService = new DecodalLanguageService(
|
languageService = new DecodalLanguageService(
|
||||||
createPlaygroundEnvironment(() => project.files),
|
createPlaygroundEnvironment(() => project.files),
|
||||||
);
|
);
|
||||||
@@ -348,22 +388,42 @@ loadExample.addEventListener('click', () => {
|
|||||||
if (!example) return;
|
if (!example) return;
|
||||||
loadExampleProject(example.id);
|
loadExampleProject(example.id);
|
||||||
});
|
});
|
||||||
|
function closeNewFileInput() {
|
||||||
|
newFileForm.classList.remove('editing');
|
||||||
|
newFilePath.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
newFile.addEventListener('click', () => {
|
newFile.addEventListener('click', () => {
|
||||||
const path = normalizePath(prompt('New virtual file path', 'schemas/types.dcdl') ?? '');
|
newFileForm.classList.add('editing');
|
||||||
if (!path) return;
|
newFilePath.value = '';
|
||||||
|
newFilePath.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
newFileForm.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const path = normalizePath(newFilePath.value);
|
||||||
|
if (!path) {
|
||||||
|
newFilePath.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeNewFileInput();
|
||||||
if (project.files[path] !== undefined) {
|
if (project.files[path] !== undefined) {
|
||||||
setActiveFile(path);
|
setActiveFile(path);
|
||||||
|
editor.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
project.files[path] = 'value = "new";\n';
|
project.files[path] = 'value = "new";\n';
|
||||||
setActiveFile(path);
|
setActiveFile(path);
|
||||||
|
editor.focus();
|
||||||
});
|
});
|
||||||
deleteFile.addEventListener('click', () => {
|
|
||||||
if (Object.keys(project.files).length <= 1) return;
|
newFilePath.addEventListener('keydown', (event) => {
|
||||||
if (!confirm(`Delete ${project.activePath}?`)) return;
|
if (event.key !== 'Escape') return;
|
||||||
delete project.files[project.activePath];
|
event.preventDefault();
|
||||||
if (project.files[project.entryPath] === undefined) {
|
closeNewFileInput();
|
||||||
project.entryPath = Object.keys(project.files).sort()[0];
|
newFile.focus();
|
||||||
}
|
});
|
||||||
setActiveFile(Object.keys(project.files).sort()[0]);
|
|
||||||
|
newFilePath.addEventListener('blur', () => {
|
||||||
|
closeNewFileInput();
|
||||||
});
|
});
|
||||||
|
|||||||
+1052
-372
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user