feat(editor): F4.6d the web browser and web server on a computer's desktop

The oracle BrowserWindowRun uses Java's browser and web server windows as a
person does; the room keeps a browser window per computer and sends a
program window's view, the start names the person's language. The editor
shows the page in a sandboxed frame, links and forms go to the room, the web
server has its button, virtual hosts and lines. Better than Java: relative
images and links count from the page's folder, a mark stays in the page, the
error page from Java's template, the virtual hosts' file with complete rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:09:05 +00:00
co-authored by Claude Opus 5.5
parent f0e7c73ec7
commit aa54e5400e
31 changed files with 2773 additions and 55 deletions
+1
View File
@@ -2,3 +2,4 @@
export * from './host.js';
export * from './sim.js';
export * from './terminal.js';
export * from './web.js';
+20 -5
View File
@@ -4,7 +4,7 @@
// 6.1). The room runs on F (RemoteSimHost, F5) or in a web worker
// (LocalSimHost); the messages are the same.
import type { CaptureRow } from '@mdg-filius/core';
import type { CaptureRow, PageLanguage } from '@mdg-filius/core';
export const SIM_PROTOCOL_VERSION = 1;
@@ -26,8 +26,13 @@ export type ClientMessageKind = (typeof CLIENT_MESSAGE_KINDS)[number];
export type ClientMessage =
| { readonly v: 1; readonly kind: 'sub'; readonly topics: readonly Topic[] }
| { readonly v: 1; readonly kind: 'unsub'; readonly topics: readonly Topic[] }
/** Starts the simulation with the document as it is now. */
| { readonly v: 1; readonly kind: 'start' }
/**
* Starts the simulation with the document as it is now; the pages the
* devices write themselves (a router's) come in the language FILIUS runs in
* for the person who starts it, as Java's come in its locale (German
* without one).
*/
| { readonly v: 1; readonly kind: 'start'; readonly language?: PageLanguage }
| { readonly v: 1; readonly kind: 'stop' }
/** Java's speed slider, 1 to 100 per cent: how long a frame takes over a cable. */
| { readonly v: 1; readonly kind: 'speed'; readonly value: number }
@@ -42,7 +47,7 @@ export type ClientMessage =
readonly reqId: string;
};
export const HOST_MESSAGE_KINDS = ['state', 'activity', 'trace', 'app', 'node', 'ack', 'error'] as const;
export const HOST_MESSAGE_KINDS = ['state', 'activity', 'trace', 'app', 'view', 'node', 'ack', 'error'] as const;
export type HostMessageKind = (typeof HOST_MESSAGE_KINDS)[number];
export type SimMode = 'design' | 'simulation';
@@ -67,6 +72,13 @@ export type HostMessage =
| { readonly v: 1; readonly kind: 'trace'; readonly portId: string; readonly frames: readonly TraceFrame[]; readonly reset: boolean }
/** A program's new window lines from line `from` on; on subscribing, all (from 0). */
| { readonly v: 1; readonly kind: 'app'; readonly node: string; readonly app: AppKind; readonly from: number; readonly lines: readonly unknown[] }
/**
* What a program's window shows besides its lines (plan §7.3), whole: on
* subscribing and when it changes; null while it has nothing (its program
* does not run, a browser that never ran). The browser's (BrowserView) and
* the web server's (WebServerView) so far.
*/
| { readonly v: 1; readonly kind: 'view'; readonly node: string; readonly app: AppKind; readonly view: unknown }
/**
* A computer's first card as the simulation runs it (DHCP may change it):
* on subscribing, when it changes, and none while the simulation never ran.
@@ -115,8 +127,11 @@ export function parseClientMessage(value: unknown): Parsed<ClientMessage> {
return { message: { v: 1, kind: kind as 'sub' | 'unsub', topics: topics as Topic[] } };
}
case 'start':
if (value.language === undefined) return { message: { v: 1, kind: 'start' } };
if (value.language !== 'de' && value.language !== 'en') return bad('badMessage', `language ${String(value.language)}`);
return { message: { v: 1, kind: 'start', language: value.language } };
case 'stop':
return { message: { v: 1, kind: kind as 'start' | 'stop' } };
return { message: { v: 1, kind: 'stop' } };
case 'speed':
if (typeof value.value !== 'number' || !Number.isFinite(value.value)) return bad('badMessage', 'speed is no number');
return { message: { v: 1, kind: 'speed', value: value.value } };
+74
View File
@@ -0,0 +1,74 @@
// What the web browser's and the web server's windows show, as the room
// keeps them and every client shows them (plan §7.3), after
// GUIApplicationWebBrowserWindow and GUIApplicationWebServerWindow
// (measured with tools/java-oracle's BrowserWindowRun).
//
// The browser: Java's address line shows "http://", then the address of each
// load as "http://" + host + path without its query (also when nothing
// comes); its title is the program's name, then that address, then the
// title of the page once it came. The page itself goes to the client as it
// came — the client shows it in a sandbox, its images from the data that
// came for them.
/** The page the browser shows. */
export type BrowserPage =
/** Java's start page: the browser's logo. */
| { readonly kind: 'start' }
/** sw_webbrowser_msg1: the server could not be reached. */
| { readonly kind: 'unreachable' }
/** A status other than 200: the browser's error page for it (tmpl/http_fehler). */
| { readonly kind: 'error'; readonly statusCode: number }
/** An HTML page, with the images that came for it by their src as written (their content type and data in Base64). */
| { readonly kind: 'html'; readonly html: string; readonly images: Readonly<Record<string, BrowserImage>> }
/** Any other text (Java shows it as HTML). */
| { readonly kind: 'text'; readonly text: string }
/** An image on its own (Java shows its Base64 as HTML). */
| { readonly kind: 'image'; readonly image: BrowserImage };
export interface BrowserImage {
readonly contentType: string;
readonly data: string;
}
export interface BrowserView {
/** The address line as the room last set it: "http://" at first and after an address that is none. */
readonly address: string;
/** The address of the last load, the title until its page came; null before the first load (the title is the program's name). */
readonly loading: string | null;
/** Whether the page shown came for the last load (its title is the window's). */
readonly arrived: boolean;
readonly page: BrowserPage;
}
/** The browser before it loaded anything. */
export const BROWSER_START: BrowserView = { address: 'http://', loading: null, arrived: false, page: { kind: 'start' } };
/** The web server's window besides its lines: the start button, the virtual hosts' check box and table (five rows of host name and folder). */
export interface WebServerView {
readonly active: boolean;
readonly useVirtualHosts: boolean;
readonly virtualHosts: readonly (readonly [string, string])[];
}
/**
* The text of an HTML page's first title, as it is written (entities not
* decoded); undefined without one or with an empty one (Java keeps the
* title it had).
*/
export function htmlTitle(html: string): string | undefined {
const match = /<title(?:\s[^>]*)?>([\s\S]*?)<\/title\s*>/i.exec(html);
if (match === null) return undefined;
const text = match[1]!.replace(/<[^>]*>/g, '');
return text.trim() === '' ? undefined : text;
}
/**
* The browser window's title, as Java's: the program's name before the
* first load, then the address of the last load, then the title of its page
* once it came, where the page has one (`pageTitle`: an HTML page's, an
* error page's in the client's language).
*/
export function browserTitle(view: BrowserView, program: string, pageTitle: (page: BrowserPage) => string | undefined): string {
if (view.loading === null) return program;
return (view.arrived ? pageTitle(view.page) : undefined) ?? view.loading;
}