From dea39cb7748f1a1942de5d39eebacb90dc216a3c Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Sun, 28 May 2023 00:08:53 +0200 Subject: [PATCH 01/12] Improve typing of state API --- docs/src/elements.html | 10 ++++++++++ src/bundles/flottplot.ts | 17 ++++------------- src/elements/animation.ts | 37 +++++++++++++++++++++++++------------ src/elements/items.ts | 13 +++++++++---- src/elements/state.ts | 29 ++++++++++++++++++++--------- src/elements/text.ts | 31 +++++++++++++++++++++---------- src/errors.ts | 1 + src/interface.ts | 5 +++-- src/manager.ts | 31 +++++++++++++++++++------------ 9 files changed, 112 insertions(+), 62 deletions(-) diff --git a/docs/src/elements.html b/docs/src/elements.html index 9a92839..fd4880c 100644 --- a/docs/src/elements.html +++ b/docs/src/elements.html @@ -682,5 +682,15 @@

Actions

+ +
+

Demo

+
+<fp-range id="x" type="dropdown" min="0" max="15" step="1"></fp-range>
+<fp-range id="y" type="dropdown" min="0" max="15" step="1"></fp-range>
+<fp-state id="s"></fp-state>
+<fp-controls target="s"></fp-controls>
+
+
diff --git a/src/bundles/flottplot.ts b/src/bundles/flottplot.ts index 4614214..54b0b3e 100644 --- a/src/bundles/flottplot.ts +++ b/src/bundles/flottplot.ts @@ -14,6 +14,8 @@ import { FPCursors } from "../elements/cursors"; import { FPOverlay } from "../elements/overlay"; import { FPPlot } from "../elements/plot"; import { FPStack } from "../elements/stack"; +import { FPState } from "../elements/state"; +import { FPText } from "../elements/text"; import { FPVideo } from "../elements/video"; import { rangeFrom, selectFrom } from "../elements/items" @@ -28,26 +30,15 @@ Flottplot.registerTag("fp-plot", FPPlot.from, false); Flottplot.registerTag("fp-range", rangeFrom, false); Flottplot.registerTag("fp-select", selectFrom, false); Flottplot.registerTag("fp-stack", FPStack.from, true); +Flottplot.registerTag("fp-state", FPState.from, false); +Flottplot.registerTag("fp-text", FPText.from, true); Flottplot.registerTag("fp-video", FPVideo.from, false); // ... export { VERSION, Flottplot, ElementMixin, Value, FlottplotError, dom }; // TODO return { -// TODO VERSION = "2.2.0", -// TODO Flottplot: Flottplot, -// TODO FPElement: FPElement, -// TODO Value: Value, // TODO OptionsItems: OptionsItems, // TODO RangeItems: RangeItems, -// TODO FlottplotError: FlottplotError, -// TODO ElementError: ElementError, -// TODO dom: dom, // TODO }; - -// Create element registry and initialize with core elements - -// TODO Flottplot.registerTag("fp-state", FPState.from, false); -// TODO Flottplot.registerTag("fp-text", FPText.from, true); - diff --git a/src/elements/animation.ts b/src/elements/animation.ts index fae197e..eb67cf4 100644 --- a/src/elements/animation.ts +++ b/src/elements/animation.ts @@ -1,8 +1,10 @@ -import { Identifier, Action, FPElement } from "../interface"; +import { Identifier, Action, FPElement, ElementState } from "../interface"; import { ElementMixin } from "../element"; import { newNode, newButton, Attributes } from "../dom"; - + +type FPAnimationState = [boolean, number]; // playing, speed + export class FPAnimation extends ElementMixin implements FPElement { readonly targets: Array; @@ -43,23 +45,34 @@ export class FPAnimation extends ElementMixin implements FPElement { ); } + get isPlaying(): boolean { + return (this.timeout != null); + } + get value(): undefined { return undefined; } - get state(): any { // TODO - return { - playing: (this.timeout != null), - speed: this.speed - }; + get state(): FPAnimationState { + return [this.isPlaying, this.speed]; } - set state(state: any) { // TODO - this.speed = state.speed; - if (state.playing) { - this.start(); + set state(state: ElementState) { // TODO + const ok = ( + Array.isArray(state) + && state.length === 2 + && typeof state[0] === "boolean" + && typeof state[1] === "number" + ); + if (ok) { + this.speed = state[1]; + if (state[0]) { + this.start(); + } else { + this.stop(); + } } else { - this.stop(); + this.fail(`cannot recover from state ${state}`); // TODO StateError } } diff --git a/src/elements/items.ts b/src/elements/items.ts index 15fcd43..7298a3f 100644 --- a/src/elements/items.ts +++ b/src/elements/items.ts @@ -1,4 +1,4 @@ -import { Identifier, Calls, FormatSpec, FPElement, Collection, CollectionEvent } from "../interface"; +import { Identifier, Calls, FormatSpec, FPElement, ElementState, Collection, CollectionEvent } from "../interface"; import { ElementError } from "../errors"; import { ElementMixin } from "../element"; import { newNode, newButton, Attributes } from "../dom"; @@ -57,6 +57,7 @@ export function selectFrom(node: HTMLElement): FPItems { +type FPItemsState = number; class FPItems extends ElementMixin implements FPElement { // Base class for control elements wrapping a Items instance. Most @@ -109,12 +110,16 @@ class FPItems extends ElementMixin implements FPElement { // (De-)Serialization - get state(): number { + get state(): FPItemsState { return this.items.index; } - set state(state: number) { - this.items.index = state; + set state(state: ElementState) { + if (typeof state === "number") { + this.items.index = state; + } else { + this.fail(`cannot recover from state ${state}`); // TODO StateError + }; } // Actions diff --git a/src/elements/state.ts b/src/elements/state.ts index bcf87ad..cdecb54 100644 --- a/src/elements/state.ts +++ b/src/elements/state.ts @@ -1,11 +1,14 @@ -import { Identifier, FPElement } from "../interface"; +import { Identifier, FPElement, ManagerState } from "../interface"; import { ElementMixin } from "../element"; import { Attributes } from "../dom"; -class FPState extends FPElement { +export class FPState extends ElementMixin implements FPElement { - constructor(id, useURL) { + private readonly useURL: boolean; + private savedState: null | ManagerState; + + constructor(id: Identifier | undefined, useURL: boolean) { super(id); this.useURL = useURL; this.savedState = null; @@ -13,27 +16,35 @@ class FPState extends FPElement { this.actions.add("restore"); } - initialize() { + override initialize() { if (this.useURL === true) { this.flottplot.urlstate = true; } } - save() { + get value(): undefined { + return undefined; + } + + get state(): undefined { + return undefined; + } + + save(): void { this.savedState = this.flottplot.state; } - restore() { + restore(): void { if (this.savedState != null) { this.flottplot.state = this.savedState; } } - static from(node) { - const attrs = dom.Attributes.from(node); + static from(node: HTMLElement): FPState { + const attrs = Attributes.from(node); return new FPState( attrs.id, - attrs.pop("url", false, "BOOL") + attrs.getAsBool("url", "false", true) ); } diff --git a/src/elements/text.ts b/src/elements/text.ts index d234958..ce42e1d 100644 --- a/src/elements/text.ts +++ b/src/elements/text.ts @@ -1,24 +1,35 @@ -import { Identifier, FPElement } from "../interface"; +import { Identifier, Substitution, FPElement } from "../interface"; import { ElementMixin } from "../element"; -import { Attributes } from "../dom"; +import { newNode } from "../dom"; -class FPText extends FPElement { +export class FPText extends ElementMixin implements FPElement { - constructor(id, text) { + override node: HTMLSpanElement; + private readonly text: string; + + constructor(id: Identifier | undefined, text: string) { super(id); - this.node = dom.newNode("span", { "id": id }); + this.node = newNode("span", { "id": id }); this.text = text; this.setDependenciesFrom(text); } - update(subst) { - let text = this.substitute(this.text, subst); - this.node.textContent = text; + override update(subst: Substitution): void { + this.node.textContent = this.substitute(this.text, subst); + } + + get value(): undefined { + return undefined; + } + + get state(): undefined { + return undefined; } - static from(node) { - return new FPText(node.id, node.textContent); + static from(node: HTMLElement): FPText { + const text = node.textContent; + return new FPText(node.id, (text == null) ? "" : text); } } diff --git a/src/errors.ts b/src/errors.ts index 260a2ef..b057233 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -2,6 +2,7 @@ export class FlottplotError extends Error {} export class ParseError extends FlottplotError {} export class FormatError extends FlottplotError {} +export class StateError extends FlottplotError {} export class ValueError extends FlottplotError {} export class ItemsError extends FlottplotError {} diff --git a/src/interface.ts b/src/interface.ts index 2fe5ba3..1729371 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -42,8 +42,8 @@ export enum CollectionEvent { } // ... -export type ElementState = any; // TODO -export type ManagerState = any; // TODO +export type ElementState = unknown; +export type ManagerState = Record; export interface Manager { // ... @@ -60,6 +60,7 @@ export interface Manager { state: ManagerState; overlay: any; // TODO fullscreen: Fullscreen; // TODO + urlstate: boolean; } export interface FPElement { diff --git a/src/manager.ts b/src/manager.ts index 9d7e69b..4fce6b1 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -1,4 +1,4 @@ -import { Identifier, Action, Calls, Substitution, Pattern, FPElement, Manager, ManagerState } from "./interface"; +import { Identifier, Action, Calls, Substitution, FPElement, Manager, ManagerState } from "./interface"; import { ElementError, FlottplotError } from "./errors"; import { UpdateGraph } from "./graph"; import * as dom from "./dom"; @@ -14,6 +14,12 @@ type TagRegistration = { isRecursive: boolean; }; + +function containsState(obj: unknown): obj is ManagerState { + return (typeof obj === "object" && !Array.isArray(obj) && obj !== null); +} + + export class Flottplot implements Manager { _elements: Map; @@ -170,7 +176,7 @@ export class Flottplot implements Manager { values.set(dep, dep_element.value); } // Evaluate expressions in all patterns with values and format - const out: Map = new Map(); + const out: Substitution = new Map(); for (const [pattern, [expression, format]] of element.patterns) { out.set(pattern, expression._eval(values).toString(format)); } @@ -225,17 +231,18 @@ export class Flottplot implements Manager { return out; } - set state(state: ManagerState) { - for (const id of this._graph.orderedNodes) { - if (!state.hasOwnProperty(id)) { - continue; + set state(state: unknown) { + if (!containsState(state)) throw new FlottplotError( + "TODO 2389" // TODO + ); + for (const id in state) { + const element = this._elements.get(id); + if (element != null) { + element.state = state[id]; + element.update(this._substitutionFor(element)); + element.notify(); + // TODO pause changing of hash during update and notify? } - // Skip null check since id comes from graph - const element: FPElement = this._elements.get(id)!; - element.state = state[id]; - element.update(this._substitutionFor(element)); - element.notify(); - // TODO pause changing of hash during update and notify? } } From 7de5fcaf8d714b7b7fecb212c96b0316b4b4b573 Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Sun, 28 May 2023 00:29:55 +0200 Subject: [PATCH 02/12] Save state of calendar element --- src/element.ts | 7 +++---- src/elements/calendar.ts | 29 ++++++++++++++++++++--------- src/values.ts | 10 +++++----- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/element.ts b/src/element.ts index 168f77a..bdf5ee8 100644 --- a/src/element.ts +++ b/src/element.ts @@ -65,13 +65,13 @@ export class ElementMixin { } // Throw an error, provides additional context generated for this element - fail(message: string): void { + fail(message: string): never { this.failWith(new ElementError( "in " + this.constructor.name + " '" + this.id + "': " + message )); } - failWith(error: Error) { + failWith(error: Error): never { if (this.node != null) { if (this._errorBox == null) { this._errorBox = newNode("div", { @@ -95,9 +95,8 @@ export class ElementMixin { // Condition ensures there is at least one child, skip null check this._errorBox.firstChild!.remove(); } - } else { - throw error; } + throw error; } // Invoke an action of the element, update the element and notify all diff --git a/src/elements/calendar.ts b/src/elements/calendar.ts index 82ec80d..f0fff15 100644 --- a/src/elements/calendar.ts +++ b/src/elements/calendar.ts @@ -1,10 +1,12 @@ -import { Identifier, FPElement } from "../interface"; +import { Identifier, FPElement, ElementState } from "../interface"; import { ElementMixin } from "../element"; -import { ValueError, ParseError } from "../errors"; +import { ValueError } from "../errors"; import { newNode, Attributes } from "../dom"; import { Value, DateValue } from "../values"; +type FPCalendarState = string; + export class FPCalendar extends ElementMixin implements FPElement { override node: HTMLInputElement; @@ -49,16 +51,25 @@ export class FPCalendar extends ElementMixin implements FPElement { ); } - get value(): Value { + get value(): DateValue { const value = Value.from(this.node.value); - if (value == null) throw new ParseError( - `unexpected issue parsing ${this.node.value} as value` - ); - return value; + if (value instanceof DateValue) { + return value; + } else { + this.fail(`unexpected issue parsing ${this.node.value} as a DateValue`); // TODO ParseError? + } } - get state(): undefined { - return undefined; // TODO + get state(): FPCalendarState { + return this.node.value; + } + + set state(state: ElementState) { + if (typeof state === "string") { + this.node.value = state; + } else { + this.fail(`cannot recover from state ${state}`); // TODO StateError + } } private get date() { diff --git a/src/values.ts b/src/values.ts index 31487cb..e05267c 100644 --- a/src/values.ts +++ b/src/values.ts @@ -58,7 +58,7 @@ export abstract class Value { export class TextValue extends Value implements Expression { - _value: string; + readonly _value: string; constructor(value: unknown) { super(); @@ -113,7 +113,7 @@ export class TextValue extends Value implements Expression { export class NumberValue extends Value implements Expression { - _value: number; + readonly _value: number; constructor(value: unknown) { super(); @@ -256,7 +256,7 @@ function asDate(value: string): Date { export class DateValue extends Value implements Expression { - _value: Date; + readonly _value: Date; YEAR: NumberValue; MONTH: NumberValue; @@ -326,7 +326,7 @@ export class DateValue extends Value implements Expression { export class DateDeltaValue extends Value implements Expression { - _value: number; + readonly _value: number; SIGN: NumberValue; DAYS: NumberValue; @@ -444,7 +444,7 @@ export class DateDeltaValue extends Value implements Expression { // Attribute names are special objects export class AttributeValue extends Value implements Expression { - _name: string; + readonly _name: string; constructor(name: string) { super(); From 57e100b294a7d62778b463a11d2f44fa978468c9 Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Sun, 28 May 2023 01:08:23 +0200 Subject: [PATCH 03/12] Distinguish between warning and failure events --- src/element.ts | 19 ++++++++++--------- src/elements/animation.ts | 2 +- src/elements/calendar.ts | 2 +- src/elements/items.ts | 3 +-- src/interface.ts | 5 +++-- src/manager.ts | 6 +++--- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/element.ts b/src/element.ts index bdf5ee8..7ace9e9 100644 --- a/src/element.ts +++ b/src/element.ts @@ -64,14 +64,11 @@ export class ElementMixin { this._manager = manager; } - // Throw an error, provides additional context generated for this element - fail(message: string): never { - this.failWith(new ElementError( - "in " + this.constructor.name + " '" + this.id + "': " + message - )); - } - - failWith(error: Error): never { + warn(message: string | Error): ElementError { + if (message instanceof Error) { + message = `${message.constructor.name}: ${message.message}`; + } + const error = new ElementError(`in ${this.constructor.name} '${this.id}': ${message}`); if (this.node != null) { if (this._errorBox == null) { this._errorBox = newNode("div", { @@ -96,7 +93,11 @@ export class ElementMixin { this._errorBox.firstChild!.remove(); } } - throw error; + return error; + } + + fail(message: string | Error): never { + throw this.warn(message); } // Invoke an action of the element, update the element and notify all diff --git a/src/elements/animation.ts b/src/elements/animation.ts index eb67cf4..c6bbd20 100644 --- a/src/elements/animation.ts +++ b/src/elements/animation.ts @@ -72,7 +72,7 @@ export class FPAnimation extends ElementMixin implements FPElement { this.stop(); } } else { - this.fail(`cannot recover from state ${state}`); // TODO StateError + this.warn(`cannot recover from state ${state}`); // TODO StateError } } diff --git a/src/elements/calendar.ts b/src/elements/calendar.ts index f0fff15..cf80afc 100644 --- a/src/elements/calendar.ts +++ b/src/elements/calendar.ts @@ -68,7 +68,7 @@ export class FPCalendar extends ElementMixin implements FPElement { if (typeof state === "string") { this.node.value = state; } else { - this.fail(`cannot recover from state ${state}`); // TODO StateError + this.warn(`cannot recover from state ${state}`); // TODO StateError } } diff --git a/src/elements/items.ts b/src/elements/items.ts index 7298a3f..9ef37ac 100644 --- a/src/elements/items.ts +++ b/src/elements/items.ts @@ -104,7 +104,6 @@ class FPItems extends ElementMixin implements FPElement { assertFinite(): void { if (!this.items.isFinite) { this.fail("list of items is not finite"); - return; } } @@ -118,7 +117,7 @@ class FPItems extends ElementMixin implements FPElement { if (typeof state === "number") { this.items.index = state; } else { - this.fail(`cannot recover from state ${state}`); // TODO StateError + this.warn(`cannot recover from state ${state}`); // TODO StateError }; } diff --git a/src/interface.ts b/src/interface.ts index 1729371..de6bbe6 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,5 +1,6 @@ import { Value } from "./values"; import { Fullscreen } from "./dom"; +import { FlottplotError } from "./errors"; // Identifiers must be usable as Map keys export type Identifier = string; @@ -80,7 +81,7 @@ export interface FPElement { update(substitution?: Substitution): void; invoke(action: Action): void; // TODO: args notify(): void; - fail(message: string): void; - failWith(error: Error): void; + warn(message: string | Error): FlottplotError; + fail(message: string | Error): never; } diff --git a/src/manager.ts b/src/manager.ts index 4fce6b1..8c60e61 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -55,8 +55,8 @@ export class Flottplot implements Manager { const duplicate = this._elements.get(element.id); // Make sure id doesn't already exist in the collection if (duplicate != null) { - element.fail("duplicate id"); - duplicate.fail("duplicate id"); + element.warn("duplicate id"); + duplicate.warn("duplicate id"); } // Add element to collection this._elements.set(element.id, element); @@ -135,7 +135,7 @@ export class Flottplot implements Manager { try { element.initialize(this._substitutionFor(element)); } catch (error) { - element.failWith(error); + element.warn(error); console.error(error); } } From 4aa97ed31eb16d1bfd5a41bc9b458e2b50492494 Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Sun, 28 May 2023 01:22:19 +0200 Subject: [PATCH 04/12] Allow use of : in format spec after the initial separator --- docs/src/values.html | 3 ++- src/element.ts | 9 +++++---- src/interface.ts | 2 +- src/values.ts | 8 ++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/src/values.html b/docs/src/values.html index e60a4f5..eab1931 100644 --- a/docs/src/values.html +++ b/docs/src/values.html @@ -208,7 +208,8 @@

Formatting

Examples

    -
  • {d:%d.%m.%Y} (d = 2023-05-27 18:00)
  • +
  • {d:%d.%m.%Y} (d = 2023-05-27T14:02:05)
  • +
  • {d:%d.%m.%Y %H:%M} (d = 2023-05-27T14:02:05)
diff --git a/src/element.ts b/src/element.ts index 7ace9e9..69f2fe6 100644 --- a/src/element.ts +++ b/src/element.ts @@ -1,4 +1,4 @@ -import { Identifier, Action, Expression, Manager, Pattern, Substitution } from "./interface"; +import { Identifier, Action, Expression, Manager, Pattern, FormatSpec, Substitution } from "./interface"; import { ElementError } from "./errors"; import { Expr } from "./expression"; import { Value } from "./values"; @@ -19,7 +19,7 @@ export class ElementMixin { readonly id: Identifier; node: HTMLElement | null; - patterns: Map; + patterns: Map; dependencies: Set; actions: Set; private _manager: Manager | null; @@ -141,7 +141,7 @@ export class ElementMixin { // them into the patterns and dependencies attributes of this element so // the supervisor can provide appropriate substitutions in updates setDependenciesFrom(...templates: Array): void { - this.patterns = new Map(); + this.patterns = new Map(); this.dependencies = new Set(); for (const template of templates) { const reg = /{.+?}/g; @@ -151,7 +151,8 @@ export class ElementMixin { break } const pattern = match[0]; - const [patExpr, format] = pattern.slice(1, -1).split(":"); + const [patExpr, ...formatParts] = pattern.slice(1, -1).split(":"); + const format = (formatParts.length > 0) ? formatParts.join(":") : undefined; const expression = Expr.parse(patExpr); // format will have undefined assigned if not given this.patterns.set(pattern, [expression, format]); diff --git a/src/interface.ts b/src/interface.ts index de6bbe6..03c6210 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -16,7 +16,7 @@ export type Substitution = Map; // Formatting specification for values -export type FormatSpec = string; +export type FormatSpec = string | undefined; export interface Expression { toString(): string; diff --git a/src/values.ts b/src/values.ts index e05267c..02be3bc 100644 --- a/src/values.ts +++ b/src/values.ts @@ -68,7 +68,7 @@ export class TextValue extends Value implements Expression { this._value = value.toString(); } - toString(spec?: string): string { + toString(spec?: FormatSpec): string { // No format specification given, return text as-is if (spec == null) { return this._value; @@ -131,7 +131,7 @@ export class NumberValue extends Value implements Expression { } } - toString(spec?: string): string { + toString(spec?: FormatSpec): string { if (spec == null) { return this._value.toString(); } @@ -284,7 +284,7 @@ export class DateValue extends Value implements Expression { this.SECOND = new NumberValue(this._value.getUTCSeconds()); } - toString(spec?: string): string { + toString(spec?: FormatSpec): string { const aspy = pystrftime(this._value, (spec != null) ? spec : "%Y-%m-%d %H:%M:%S"); if (aspy != null) { return aspy; @@ -377,7 +377,7 @@ export class DateDeltaValue extends Value implements Expression { this.TOTAL_SECONDS = new NumberValue(this._value); } - toString(spec?: string): string { + toString(spec?: FormatSpec): string { // User should use number formatting options of attributes instead if (spec != null) throw new FormatError( "invalid specification '" + spec + "' for date delta value '" + this.toString() + "'" From ba11ed2e3d58cb3b2dffa0ab304cf457d2838ade Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Sun, 28 May 2023 11:03:41 +0200 Subject: [PATCH 05/12] Fix scrolling issues of documentation page layout --- docs/src/docs.less | 253 +++++++++++++++++++++++------------------ docs/src/template.html | 31 +++-- 2 files changed, 157 insertions(+), 127 deletions(-) diff --git a/docs/src/docs.less b/docs/src/docs.less index 74ed603..b127d1c 100644 --- a/docs/src/docs.less +++ b/docs/src/docs.less @@ -14,12 +14,6 @@ body { color: #000; font-family: sans-serif; } -div.row { - display: flex; - flex-wrap: wrap; - max-width: 1300px; -} - a { color: darken(@def-dark, 20%); text-decoration: underline; @@ -29,114 +23,148 @@ a:hover { text-decoration: none; } -div.sidebar { - flex-grow: 1; - width: 200px; - height: 100vh; - position: sticky; - top: 0; - background-color: #EEE; - padding: 0 10px; -} -div.sidebar > ul { list-style: none; padding-left: 0; margin: 0; } -div.sidebar > ul > li { padding: 3px 0; margin-bottom: 5px; } -div.sidebar > ul > li > a { font-weight: bold; } -div.sidebar > ul > li > ul { list-style: none; padding-left: 15px; } -div.sidebar > ul > li > ul > li { } -div.sidebar a { color: #000; display: block; text-decoration: none; } -div.sidebar a:hover { color: @def-dark; } - -div.content { - flex-basis: 0; - flex-grow: 999; - min-inline-size: 600px; - padding: 10px; - background-color: #FFF; - z-index: 10; +div.row { + display: flex; + flex-wrap: wrap; + max-width: 1300px; + + .sidebar { + flex-grow: 1; + width: 200px; + background-color: #EEE; + padding: 10px; + } + + .content { + flex-basis: 0; + flex-grow: 999; + min-width: 600px; + padding: 10px 10px 10px 20px; + background-color: #FFF; + z-index: 10; + } } -div.header { - display: block; - margin: 10px -10px 10px 0; - padding: 0 10px 10px 0; +.ui-logo { border-bottom: 2px solid #999; - color: #999; - height: 30px; -} -#logo { - height: 30px; - max-width: 100%; + + img { + display: block; + height: 30px; + max-width: 100%; + } } -#navbar { + +.ui-nav { font-size: 130%; - margin: 0 0 10px -10px; - padding: 0px 0 10px 10px; border-color: #CCC; text-align: right; -} -#navbar a { - display: inline-block; - padding: 0 7px; - color: black; - text-decoration: none; -} -#navbar a:hover { - color: @def-dark; -} + border-bottom: 2px solid #CCC; -main { padding: 0 0 0 10px; margin: 0; } + a { + display: inline-block; + padding: 0 7px; + color: black; + text-decoration: none; + } + a:hover { + color: @def-dark; + } -main h1 { - font-size: 200%; - margin: 30px 0 10px 0; - font-weight: normal; -} -main h2 { - font-size: 140%; - margin: 30px 0 10px 0; - border-bottom: 2px solid #CCC; - padding-bottom: 5px; - font-weight: normal; } -main h3 { - font-size: 100%; - margin: 20px 0 10px 0; -} -main ul { - margin: 10px 0; - padding-left: 30px; - list-style: none; -} -main p { - margin: 10px 0; -} -main p:first-child { - margin-top: revert; -} -main ul>li::before { - content: "●"; /* Add content: \2022 is the CSS Code/unicode for a bullet */ - display: inline-block; /* Needed to add space between the bullet and the text */ - width: 20px; /* Also needed for space (tweak if needed) */ - margin-left: -20px; /* Also needed for space (tweak if needed) */ - color: #999; -} -main ul li ul { - margin: 0 0 10px 0; -} -main pre { - margin: 10px 0; - font-size: 1.1em; - overflow: scroll; - padding: 8px 10px; - border-radius: 3px; - background-color: #EEE; + +.ui-toc { + /* wrap happens at 200px (sidebar) + 600px (content) + 50px (padding) */ + @media only screen and (min-width: 850px) { + height: calc(100vh - 20px); + position: sticky; + top: 0; + overflow-y: scroll; + } + + > ul { + list-style: none; + padding-left: 0; + margin: 0; + > li { + padding: 3px 0; + margin-bottom: 5px; + > a { + font-weight: bold; + } + > ul { + list-style: none; + padding-left: 15px; + } + } + } + + a { + color: #000; + display: block; + text-decoration: none; + } + a:hover { + color: @def-dark; + } } -main code { - display: inline-block; - background-color: #EEE; - font-size: 1.2em; - padding: 0 4px; - border-radius: 3px; + +.ui-main { + h1 { + font-size: 200%; + margin: 30px 0 10px 0; + font-weight: normal; + } + h2 { + font-size: 140%; + margin: 30px 0 10px 0; + border-bottom: 2px solid #CCC; + padding-bottom: 5px; + font-weight: normal; + } + h3 { + font-size: 100%; + margin: 20px 0 10px 0; + } + + ul { + margin: 10px 0; + padding-left: 30px; + list-style: none; + > li::before { + content: "●"; /* Add content: \2022 is the CSS Code/unicode for a bullet */ + display: inline-block; /* Needed to add space between the bullet and the text */ + width: 20px; /* Also needed for space (tweak if needed) */ + margin-left: -20px; /* Also needed for space (tweak if needed) */ + color: #999; + } + > li ul { + margin: 0 0 10px 0; + } + } + + p { + margin: 10px 0; + } + p:first-child { + margin-top: revert; + } + + pre { + margin: 10px 0; + font-size: 1.1em; + overflow-x: scroll; + padding: 8px 10px; + border-radius: 3px; + background-color: #EEE; + } + code { + display: inline-block; + background-color: #EEE; + font-size: 1.2em; + padding: 0 4px; + border-radius: 3px; + } } .downloads a { @@ -148,46 +176,49 @@ main code { h3 { color: @def-dark; } - >ul>li::before { + > ul > li::before { color: @def-dark; } - pre, >ul>li>code:first-child { + pre, > ul > li > code:first-child { background-color: @def-light; } } + .val { border-color: @val-light; h3 { color: @val-dark; } - >ul>li::before { + > ul > li::before { color: @val-dark; } - pre, >ul>li>code:first-child { + pre, > ul > li > code:first-child { background-color: @val-light; } } + .act { border-color: @act-light; h3 { color: @act-dark; } - >ul>li::before { + > ul > li::before { color: @act-dark; } - pre, >ul>li>code:first-child { + pre, > ul > li > code:first-child { background-color: @act-light; } } + .demo { border-color: @demo-light; h3 { color: @demo-dark; } - >ul>li::before { + > ul > li::before { color: @demo-dark; } - pre, >ul>li>code:first-child { + pre, > ul > li > code:first-child { background-color: @demo-light; } } diff --git a/docs/src/template.html b/docs/src/template.html index c79aca0..b6fade4 100644 --- a/docs/src/template.html +++ b/docs/src/template.html @@ -10,26 +10,25 @@ - +
+ + +
- From ec90ddc77a21323048aaba2c825e9a7586554dcf Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Sun, 28 May 2023 20:55:42 +0200 Subject: [PATCH 06/12] Set up dependency generator for make --- .gitignore | 7 +++--- Makefile | 22 +++++++++++++---- util/generate_dependencies.py | 46 +++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 util/generate_dependencies.py diff --git a/.gitignore b/.gitignore index 290c3f1..14085f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ *todo* -dist/* -tests/* + +deps +dist docs/*.html docs/*.css docs/dist docs/plot +node_modules test/flottplot.js test/*.json -node_modules diff --git a/Makefile b/Makefile index a170eb3..1763067 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ dist: \ dist/flottplot.css clean: + rm -rf deps rm -rf dist rm -rf docs/*.html rm -rf docs/*.css @@ -25,13 +26,22 @@ clean: dist/%.css: src/%.less | dist/ npx lessc $< > $@ -dist/%-min.js: src/bundles/%.ts | dist/ +# Building a bundle with webpack: +# - bundle js file depends on its corresponding ts file only +# - ts file dependencies are auto-generated +# - webpack won't overwrite the js file if it's content hasn't changed, +# touching it anyway after successful compilation ensures that make +# recognizes the update +dist/%-min.js: src/bundles/%.ts | dist/ deps/ + python3 util/generate_dependencies.py $< > deps/$*.mk npx webpack build \ --entry-reset \ --entry "./$<" \ - --output-filename "$(notdir $@)" + --output-filename "$(notdir $@)" \ + && touch $@ -# TODO: ts file dependencies +# Auto-generated dependencies for bundle files +-include deps/*.mk # Unit tests @@ -44,13 +54,15 @@ test: test/flottplot.js test/format_cases.json $(TESTS) test/format_cases.json: util/generate_format_cases.py | test/ python3 $< > $@ -test/flottplot.js: src/bundles/flottplot-test.ts +test/flottplot.js: src/bundles/flottplot-test.ts | deps/ + python3 util/generate_dependencies.py $< > deps/flottplot-test.mk npx webpack build \ --entry-reset \ --entry "./$<" \ --output-library-type "commonjs2" \ --output-path "test" \ - --output-filename "flottplot.js" + --output-filename "flottplot.js" \ + && touch $@ # Documentation diff --git a/util/generate_dependencies.py b/util/generate_dependencies.py new file mode 100644 index 0000000..e448b37 --- /dev/null +++ b/util/generate_dependencies.py @@ -0,0 +1,46 @@ +import argparse +from pathlib import Path +import os +import re + +EXT = ".ts" +IMPORT = re.compile(r"^import .* from \"(.+)\";?$"); + + +def get_deps(path): + out = set() + with open(path, "r") as f: + for line in f: + match = IMPORT.match(line); + if match is not None: + out.add(Path(match.group(1) + EXT)) + return out + +def get_deps_recursive(path, checked=None): + path = Path(path).resolve() + if checked is None: + checked = set() + checked.add(path) + for dep in get_deps(path): + dep = (path.parent / dep).resolve() + if dep not in checked: + get_deps_recursive(dep, checked) + return checked + + +parser = argparse.ArgumentParser() +parser.add_argument("entry") + +if __name__ == "__main__": + args = parser.parse_args() + + entry = Path(args.entry).resolve() + + deps = get_deps_recursive(entry) + deps.remove(entry) + deps = [os.path.relpath(dep) for dep in sorted(deps)] + deps = " ".join(deps) + + print(f"{os.path.relpath(entry)}: {deps}") + print(f"\ttouch $@") + From 8e0f0234f2018b2c446ceb5ec93e94228faed394 Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Wed, 2 Aug 2023 20:19:31 +0200 Subject: [PATCH 07/12] Use TEXT representation of numbers as fallback when no format specification given --- src/values.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/values.ts b/src/values.ts index 02be3bc..450899a 100644 --- a/src/values.ts +++ b/src/values.ts @@ -133,7 +133,7 @@ export class NumberValue extends Value implements Expression { toString(spec?: FormatSpec): string { if (spec == null) { - return this._value.toString(); + return this.TEXT.toString(); } const aspy = pyformat(this._value, spec); if (aspy != null) { From 6326888ef3a078d1dc8b11c17e396d62c3121322 Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Thu, 3 Aug 2023 19:52:12 +0200 Subject: [PATCH 08/12] Introduce _typeName and avoid usage of .constructor.name which is mangled --- src/element.ts | 7 ++--- src/elements/calendar.ts | 2 +- src/errors.ts | 8 ++++- src/expression.ts | 2 +- src/items.ts | 32 ++++++++++---------- src/manager.ts | 1 - src/values.ts | 64 ++++++++++++++++++++++++++++++++-------- 7 files changed, 81 insertions(+), 35 deletions(-) diff --git a/src/element.ts b/src/element.ts index 69f2fe6..b22e178 100644 --- a/src/element.ts +++ b/src/element.ts @@ -34,8 +34,7 @@ export class ElementMixin { } else if (/^[A-Za-z][A-Za-z0-9_]*$/.test(id)) { this.id = id; } else throw new ElementError( - "invalid id '" + id + "' for " + this.constructor.name - + " (names must begin with A-z and only contain A-z, 0-9 and _)" + `invalid element id '${id}' (names must begin with A-z and only contain A-z, 0-9 and _)` ); this.node = null; // Element is initially not connected to a flottplot supervisor. This @@ -66,9 +65,9 @@ export class ElementMixin { warn(message: string | Error): ElementError { if (message instanceof Error) { - message = `${message.constructor.name}: ${message.message}`; + message = message.message; } - const error = new ElementError(`in ${this.constructor.name} '${this.id}': ${message}`); + const error = new ElementError(`in element '${this.id}': ${message}`); if (this.node != null) { if (this._errorBox == null) { this._errorBox = newNode("div", { diff --git a/src/elements/calendar.ts b/src/elements/calendar.ts index cf80afc..4b944d7 100644 --- a/src/elements/calendar.ts +++ b/src/elements/calendar.ts @@ -21,7 +21,7 @@ export class FPCalendar extends ElementMixin implements FPElement { } else if (init instanceof DateValue) { this.resetValue = init.toString("%Y-%m-%d"); } else throw new ValueError( - "cannot initialize calendar with " + init.constructor.name + "cannot initialize calendar with " + init._typeName ); // HTML offers an input type with a nice date selector if (attrs == null) { diff --git a/src/errors.ts b/src/errors.ts index b057233..6fab60d 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,4 +1,10 @@ -export class FlottplotError extends Error {} +export class FlottplotError extends Error { + + constructor(message?: string) { + super(`Flottplot Error: ${message}`); + } + +} export class ParseError extends FlottplotError {} export class FormatError extends FlottplotError {} diff --git a/src/expression.ts b/src/expression.ts index d9e0b4b..74a9bda 100644 --- a/src/expression.ts +++ b/src/expression.ts @@ -272,7 +272,7 @@ export class Expr implements Expression { // Both forward and backward operators either don't exist or return // undefined. The operation is not possible. throw new ValueError( - "operator '" + this.op.method + "' not defined for " + args.map(_ => _.constructor.name).join(" and ") + "operator '" + this.op.method + "' not defined for " + args.map(_ => _._typeName).join(" and ") ); } diff --git a/src/items.ts b/src/items.ts index 25684c8..2e36022 100644 --- a/src/items.ts +++ b/src/items.ts @@ -133,6 +133,9 @@ export class RangeItems extends Items implements Collection { _factor: Value; _selected: number; + readonly valueType: any; + readonly valueTypeName: string; + indexMin: number; indexMax: number; @@ -156,16 +159,19 @@ export class RangeItems extends Items implements Collection { "range requires specification of at least one of init, min or max" ); // The type of this._offset determines the type of all values this - // range produces. This type is accessible via the valueType property - // for instanceof comparisons. First, verify that min and max have the - // appropriate type. init does not need to be checked (if it is not - // null, it is this._offset). step is allowed to have a different type - // (e.g., the step between dates is a datedelta). + // range produces. Make this type accessible via the valueType property + // for instanceof comparisons. + this.valueType = this._offset.constructor; + this.valueTypeName = this._offset._typeName; + // Verify that min and max have the appropriate type. init does not + // need to be checked (if it is not null, it is this._offset). step is + // allowed to have a different type (e.g., the step between dates is + // a datedelta). if (!(min == null || min instanceof this.valueType)) throw new ItemsError( - "min is a " + min.constructor.name + " but range expects " + this.valueType.name + "min is a " + min._typeName + " value but range expects " + this.valueTypeName ); if (!(max == null || max instanceof this.valueType)) throw new ItemsError( - "max is a " + max.constructor.name + " but range expects " + this.valueType.name + "max is a " + max._typeName + " value but range expects " + this.valueTypeName ); // TODO if (step == null) throw new ItemsError( @@ -188,10 +194,6 @@ export class RangeItems extends Items implements Collection { this.value = this._offset; } - get valueType() { - return this._offset.constructor; - } - _genValue(index: number): Value { // TODO: attach index? // Because this is not going through proper Expression evaluation, the @@ -202,12 +204,12 @@ export class RangeItems extends Items implements Collection { } _genIndex(value: unknown): number { - value = Value.from(value); - if (value instanceof this.valueType) { + const v = Value.from(value); + if (v instanceof this.valueType) { // Clip value into range - return Math.round((value as any)._sub(this._offset)._div(this._factor)._value); // TODO any + return Math.round((v as any)._sub(this._offset)._div(this._factor)._value); // TODO any } else throw new ItemsError( - `range expects ${this.valueType.name} but received ${value!.constructor.name}` + `range expects ${this.valueTypeName} value but received ${v!._typeName}` ); } diff --git a/src/manager.ts b/src/manager.ts index 8c60e61..763c594 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -102,7 +102,6 @@ export class Flottplot implements Manager { node.replaceWith(dom.newNode("div", { "style": "border:3px solid #F00;background-color:#FCC;padding:3px;", }, [ - dom.newNode("b", {}, [error.constructor.name, ": "]), error.message ])); // Additionally log the error on the console diff --git a/src/values.ts b/src/values.ts index 450899a..e452cb6 100644 --- a/src/values.ts +++ b/src/values.ts @@ -8,6 +8,7 @@ export abstract class Value { _TEXT?: string; abstract toString(spec?: FormatSpec): string; + abstract readonly _typeName: string; // Every value gets a user-accessible TEXT attribute, which should be set // to the raw text input used to generate the value. If the value wasn't @@ -68,6 +69,10 @@ export class TextValue extends Value implements Expression { this._value = value.toString(); } + get _typeName(): string { + return "Text"; + } + toString(spec?: FormatSpec): string { // No format specification given, return text as-is if (spec == null) { @@ -131,10 +136,17 @@ export class NumberValue extends Value implements Expression { } } + get _typeName(): string { + return "Number"; + } + toString(spec?: FormatSpec): string { + // No format specification given: return value as it was entered by the + // user or fall back to default representation if (spec == null) { - return this.TEXT.toString(); + return (this._TEXT != null) ? this._TEXT : this._value.toString(); } + // Try to apply format specification const aspy = pyformat(this._value, spec); if (aspy != null) { return aspy; @@ -284,8 +296,21 @@ export class DateValue extends Value implements Expression { this.SECOND = new NumberValue(this._value.getUTCSeconds()); } + get _typeName(): string { + return "Date"; + } + toString(spec?: FormatSpec): string { - const aspy = pystrftime(this._value, (spec != null) ? spec : "%Y-%m-%d %H:%M:%S"); + // If no format specification is given try to return original user + // input or supply the default specification: YYYY-mm-dd HH:MM:SS + if (spec == null) { + if (this._TEXT != null) { + return this._TEXT; + } else { + spec = "%Y-%m-%d %H:%M:%S"; + } + } + const aspy = pystrftime(this._value, spec); if (aspy != null) { return aspy; } @@ -377,19 +402,30 @@ export class DateDeltaValue extends Value implements Expression { this.TOTAL_SECONDS = new NumberValue(this._value); } + get _typeName(): string { + return "DateDelta"; + } + toString(spec?: FormatSpec): string { - // User should use number formatting options of attributes instead - if (spec != null) throw new FormatError( + // If no format specification is given try to return original user + // input or fall back to a default format: ±DDd HH:MM:SS + if (spec == null) { + if (this._TEXT != null) { + return this._TEXT; + } else { + return ( + (this.SIGN._value < 0 ? "-" : "+") + + this.DAYS.toString() + "d " + + this.HOURS.toString("0>2") + ":" + + this.MINUTES.toString("0>2") + ":" + + this.SECONDS.toString("0>2") + ); + } + } + // For now, users should use attributes and number formatting + throw new FormatError( "invalid specification '" + spec + "' for date delta value '" + this.toString() + "'" ); - // Return in format '±DDd HH:MM:SS' - return ( - (this.SIGN._value < 0 ? "-" : "+") - + this.DAYS.toString() + "d " - + this.HOURS.toString("0>2") + ":" - + this.MINUTES.toString("0>2") + ":" - + this.SECONDS.toString("0>2") - ); } _pos(): DateDeltaValue { @@ -454,6 +490,10 @@ export class AttributeValue extends Value implements Expression { ); } + get _typeName(): string { + return "Attribute"; + } + toString(spec?: string): string { if (spec != null) throw new FormatError("sdjfjasdf"); // TODO return this._name; From e4e57d3624887911f825efc34e476f39bc83b556 Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Thu, 3 Aug 2023 19:56:33 +0200 Subject: [PATCH 09/12] Fix docs and tests after 8e0f0234f2018b2c446ceb5ec93e94228faed394 --- docs/src/values.html | 4 ++-- test/test_values.js | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/src/values.html b/docs/src/values.html index eab1931..933438e 100644 --- a/docs/src/values.html +++ b/docs/src/values.html @@ -202,7 +202,7 @@

Formatting

  • %%: A literal percent character.
  • - If no specification is given, Date values default to %Y-%m-%d %H:%M:%S. + If no specification is given, Date values default to the original input text or %Y-%m-%d %H:%M:%S if none exists.

    @@ -291,7 +291,7 @@

    Examples

    Formatting

    DateDelta does not provide formatting options at the moment. - By default values are formatted as +Dd HH:MM:SS. + By default values are returned as they were input or are formatted as +Dd HH:MM:SS if no original input exists. Use attributes to assemble a custom format.

    diff --git a/test/test_values.js b/test/test_values.js index 6ee0a87..ce64853 100644 --- a/test/test_values.js +++ b/test/test_values.js @@ -120,11 +120,17 @@ describe("Value formatting", function () { }); it("DateDeltaValue default format", function() { - let pos = new DateDeltaValue("+5m"); + const pos = new DateDeltaValue("+5m"); + assert.strictEqual(pos.toString(), "+5m"); + pos._TEXT = null; // TODO create a proper mechanism to obtain default formatting assert.strictEqual(pos.toString(), "+0d 00:05:00"); - let neg = new DateDeltaValue("-251h"); + const neg = new DateDeltaValue("-251h"); + assert.strictEqual(neg.toString(), "-251h"); + neg._TEXT = null; // TODO create a proper mechanism to obtain default formatting assert.strictEqual(neg.toString(), "-10d 11:00:00"); - let zero = new DateDeltaValue("0d"); + const zero = new DateDeltaValue("0d"); + assert.strictEqual(zero.toString(), "0d"); + zero._TEXT = null; // TODO create a proper mechanism to obtain default formatting assert.strictEqual(zero.toString(), "+0d 00:00:00"); }); From f7fb3be4066477dc9108e72d393a7b2c9944e40d Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Sun, 9 Feb 2025 11:26:50 +0100 Subject: [PATCH 10/12] Update packages/requirements --- Makefile | 4 ++++ docs/requirements.txt | 4 ++++ package.json | 12 ++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 docs/requirements.txt diff --git a/Makefile b/Makefile index 1763067..14b3a0e 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,10 @@ clean: %/: mkdir -p $@ +init: + npm install --include dev + pip install -r docs/requirements.txt + # Flottplot modules diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..0399ef5 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +numpy +matplotlib +flottplot + diff --git a/package.json b/package.json index 114c708..885504c 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "devDependencies": { - "less": "^4.1.3", - "mocha": "^10.2.0", - "ts-loader": "^9.4.2", - "typescript": "^5.0.4", - "webpack": "^5.82.0", - "webpack-cli": "^5.1.0" + "less": "^4.2.2", + "mocha": "^11.1.0", + "ts-loader": "^9.5.2", + "typescript": "^5.7.3", + "webpack": "^5.97.1", + "webpack-cli": "^6.0.1" } } From c485ce3a1a251e5dbac145f77a73614ea73b4e5e Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Sun, 9 Feb 2025 11:48:46 +0100 Subject: [PATCH 11/12] Set up docs action --- .github/workflows/docs.yml | 34 ++++++++++++++++++++ .gitignore | 5 +-- Makefile | 60 ++++++++++++++++++------------------ docs/.nojekyll | 1 - docs/{ => html}/convert.js | 0 docs/{ => html}/favicon.png | Bin docs/{ => html}/logo.svg | 0 7 files changed, 65 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 docs/.nojekyll rename docs/{ => html}/convert.js (100%) rename docs/{ => html}/favicon.png (100%) rename docs/{ => html}/logo.svg (100%) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..be9efd7 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,34 @@ +name: docs + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - run: make init + - run: make test + - run: make docs + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/html/ + + deploy: + needs: build + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 14085f6..bed8153 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,7 @@ deps dist -docs/*.html -docs/*.css -docs/dist -docs/plot +docs/html node_modules test/flottplot.js test/*.json diff --git a/Makefile b/Makefile index 14b3a0e..08beea7 100644 --- a/Makefile +++ b/Makefile @@ -71,45 +71,45 @@ test/flottplot.js: src/bundles/flottplot-test.ts | deps/ # Documentation -DOCS := \ - docs/ \ - docs/index.html \ - docs/tutorial.html \ - docs/elements.html \ - docs/values.html \ - docs/python.html \ - docs/docs.css \ - docs/convert.js \ - docs/dist/flottplot-min.js \ - docs/dist/flottplot.css \ - docs/dist/flottplot-scan-min.js \ - docs/plot/sin-1x.png \ - docs/plot/sin-2x.png \ - docs/plot/sin-3x.png \ - docs/plot/cos-3x.png \ - docs/plot/cos-2x.png \ - docs/plot/cos-3x.png \ - docs/plot/adv_fwd_000.png \ - docs/plot/adv_bwd_000.png \ - docs/plot/adv_lag_000.png - -docs: $(DOCS) - -docs/%.css: docs/src/%.less +DOCS_HTML := \ + docs/html/ \ + docs/html/index.html \ + docs/html/tutorial.html \ + docs/html/elements.html \ + docs/html/values.html \ + docs/html/python.html \ + docs/html/docs.css \ + docs/html/convert.js \ + docs/html/dist/flottplot-min.js \ + docs/html/dist/flottplot.css \ + docs/html/dist/flottplot-scan-min.js \ + docs/html/plot/sin-1x.png \ + docs/html/plot/sin-2x.png \ + docs/html/plot/sin-3x.png \ + docs/html/plot/cos-3x.png \ + docs/html/plot/cos-2x.png \ + docs/html/plot/cos-3x.png \ + docs/html/plot/adv_fwd_000.png \ + docs/html/plot/adv_bwd_000.png \ + docs/html/plot/adv_lag_000.png + +docs: $(DOCS_HTML) + +docs/html/%.css: docs/src/%.less npx lessc $< $@ -docs/%.html: docs/util/build.py docs/src/template.html docs/src/%.html +docs/html/%.html: docs/util/build.py docs/src/template.html docs/src/%.html python3 $+ > $@ -docs/dist/%: dist/% | docs/dist/ +docs/html/dist/%: dist/% | docs/html/dist/ cp $^ $@ -docs/plot/sin-%x.png: docs/util/plot-trigonometric.py | docs/plot/ +docs/html/plot/sin-%x.png: docs/util/plot-trigonometric.py | docs/html/plot/ python3 $< "sin" $* $@ -docs/plot/cos-%x.png: docs/util/plot-trigonometric.py | docs/plot/ +docs/html/plot/cos-%x.png: docs/util/plot-trigonometric.py | docs/html/plot/ python3 $< "cos" $* $@ -docs/plot/adv_%_000.png: docs/util/plot-advection.py | docs/plot/ +docs/html/plot/adv_%_000.png: docs/util/plot-advection.py | docs/html/plot/ python3 $< $* $(dir $@) diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index 8d1c8b6..0000000 --- a/docs/.nojekyll +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/convert.js b/docs/html/convert.js similarity index 100% rename from docs/convert.js rename to docs/html/convert.js diff --git a/docs/favicon.png b/docs/html/favicon.png similarity index 100% rename from docs/favicon.png rename to docs/html/favicon.png diff --git a/docs/logo.svg b/docs/html/logo.svg similarity index 100% rename from docs/logo.svg rename to docs/html/logo.svg From f5f8e4be54ed99e0b329ae151606e95c34a303e2 Mon Sep 17 00:00:00 2001 From: Christopher Polster Date: Sun, 9 Feb 2025 20:45:55 +0100 Subject: [PATCH 12/12] Clarify usage of slicing operators --- docs/src/values.html | 8 +++++--- test/test_values.js | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/src/values.html b/docs/src/values.html index 933438e..78f42fa 100644 --- a/docs/src/values.html +++ b/docs/src/values.html @@ -45,10 +45,10 @@

    Binary Operators

    Concatenation.
  • § (Number, Text) → Text
    - Left slice. + Left slice: only retain characters after the position specified by the left argument (positive: count from left; negative: count from right).
  • § (Text, Number) → Text
    - Right slice. + Right slice: only retain characters before the position specified by the right argument (positive: count from left; negative: count from right).
  • @@ -56,8 +56,10 @@

    Binary Operators

    Examples

      -
    • {t § 3} (t = foobar)
    • {3 § t} (t = foobar)
    • +
    • {-2 § t} (t = foobar)
    • +
    • {t § 3} (t = foobar)
    • +
    • {t § -2} (t = foobar)
    • {(2 § t) § 3} (t = foobar)
    • {2 § (t § 3)} (t = foobar)
    diff --git a/test/test_values.js b/test/test_values.js index ce64853..108c37b 100644 --- a/test/test_values.js +++ b/test/test_values.js @@ -167,6 +167,11 @@ describe("Value operators", function () { ["-3 § foo ", "ijk"], [" foo § 3", "abc"], [" foo § -3", "abcdefgh"], + [" 3 § foo § 3", "def"], + ["-3 § foo § 1", "i"], + [" 3 § foo § -2", "defghi"], + ["-3 § foo § -2", "i"], + ["-3 § foo § -4", ""] ], [ ["foo", new TextValue("abcdefghijk")] ]));