From fb24cad070ad385b0790b574e6cd7e20ea147e6f Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 24 Feb 2026 11:26:16 +0100 Subject: [PATCH] - --- .gitignore | 1 + scribblings/sprintf.scrbl | 727 +++++++------------------------------- 2 files changed, 124 insertions(+), 604 deletions(-) diff --git a/.gitignore b/.gitignore index 39a4f9c..71e313a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.rkt.bak \#*.rkt# \#*.rkt#*# +*.scrbl~ # Compiled racket bytecode compiled/ diff --git a/scribblings/sprintf.scrbl b/scribblings/sprintf.scrbl index 290efa3..de0f9b4 100644 --- a/scribblings/sprintf.scrbl +++ b/scribblings/sprintf.scrbl @@ -2,654 +2,173 @@ @(require scribble/manual ;scribble/class + scribble/eval (for-label racket/base - racket/class - racket/gui/base - net/sendurl - json - ;gregor - ;gregor/time - net/sendurl - racket/path - "../private/web-wire.rkt" - "../private/webui-wire-download.rkt" - "../private/webui-wire-ipc.rkt" - "../private/css.rkt" - "../private/menu.rkt" - "../private/web-racket.rkt")) + )) -@title{High-Level Web Racket GUI API} +@(define the-eval + (make-base-eval)) + + +@title{sprintf functions} @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] -@defmodule[web-racket] +@defmodule[sprintf] -The @racket[web-racket] module provides a higher-level GUI layer on top -of the low-level @racketmodname[web-wire] backend. It is intended as the -main entry point for desktop applications that embed a WebUI. +This module provides a compact @tt{printf}-style string +formatting facility for Racket. +It is inspired by C-style format strings, but implemented +on top of @racket[racket/format]. -The module exports: +The core functionality is provided by @racket[sprintf] +and @racket[sprintf*]. + +@section{Overview} + +The module exports two formatting functions: @itemlist[ - @item{OO wrappers for DOM elements and form inputs - (@racket[ww-element%], @racket[ww-input%]);} - @item{classes for main windows and dialogs - (@racket[ww-webview%], @racket[ww-webview-dialog%], - @racket[ww-webview-message%]);} - @item{an abstract settings interface (@racket[ww-settings%]);} - @item{backend control and logging helpers re-exported from - @racketmodname[web-wire];} - @item{backend installation/override helpers re-exported from - @racketmodname[webui-wire-download];} - @item{CSS and menu construction helpers re-exported from - @racket["css.rkt"] and @racket["menu.rkt"].} + @item{@racket[sprintf] — formats using variadic arguments} + @item{@racket[sprintf*] — formats using an explicit argument list} ] -The goal of this layer is: +Both functions use the same format-string syntax and semantics. + +@section{Exports} + +@defproc[(sprintf [format string?] [arg any/c] ...) string?]{ +Formats the string @racket[format] by substituting the given +arguments according to printf-style conversion specifications. + +Arguments are consumed sequentially from left to right. +An error is raised if there are too many or too few arguments +for the format string. +} + +@defproc[(sprintf* [format string?] [args list?]) string?]{ +Variant of @racket[sprintf] where the arguments are supplied +explicitly as a list. + +This form is useful when arguments are constructed dynamically +or passed through higher-order functions. +} + +@section{Format String Syntax} + +A format specification has the general form: + +@verbatim{ +%[flags][width][.precision][length]type +} + +Only a well-defined subset of the traditional @tt{printf} +syntax is supported. + +@section{Supported Conversion Types} + +The following conversion specifiers are recognized: @itemlist[ - @item{You rarely call @racket[web-wire] directly;} - @item{you work with windows, elements, and inputs as Racket objects;} - @item{only when you need fine-grained control do you drop to the - underlying wire API.} + @item{@tt{%d} — decimal integer} + @item{@tt{%x} — hexadecimal integer} + @item{@tt{%f} — floating-point number} + @item{@tt{%s} — string} + @item{@tt{%%} — literal percent sign} ] -@section{Re-exported backend control and logging} +@section{Flags} -The following bindings are re-exported directly from -@racketmodname[web-wire]. See that module’s documentation for full -details; here is a short summary for convenience. - -@defproc[(ww-start [log-level symbol?]) web-rkt?]{ - -Start the Web Wire backend if it is not running already, and return the -current backend handle. - -Most applications do not call this directly: @racket[ww-webview%] -constructors will automatically call @racket[ww-start] when the first -window is created. -} - -@defproc[(ww-stop) void?]{ - -Stop the Web Wire backend. - -This is called automatically when the last window created via -@racket[ww-webview%] is closed; you normally do not need to call it -directly, but you can use it for explicit shutdown. -} - -@defproc[(ww-set-debug [on? boolean?]) void?]{ - -Enable or disable internal debug logging in the @racket[web-wire] -layer. This affects @racket[ww-debug] output. -} - -@defform*[[(ww-debug msg-expr) #:contracts ([msg-expr any/c]) - (ww-debug id-expr msg-expr) #:contracts ([id-expr any/c] [msg-expr any/c]) - ]]{ -Debug logging macros that write to the shared log buffer when debug is -enabled. -} - -@defform*[[(ww-error msg-expr) #:contracts ([msg-expr any/c]) - (ww-error id-expr msg-expr) #:contracts ([id-expr any/c] [msg-expr any/c]) - ]]{ -Error logging macros that always log, regardless of the debug setting. -} - -@defproc[(ww-display-log - [filter (or/c #f string? regexp? (listof string?)) #f]) - void?]{ - -Display the in-memory log buffer on @racket[current-output-port], -optionally filtered. -} - -@defproc[(ww-tail-log [args any/c] ...) void?]{ - -Show the tail of the log buffer and follow new entries (like -@tt{tail -f}). Optional arguments control how many lines to show -initially and which lines to filter. -} - -@section{Re-exported WebUI helper command control} - -The following bindings are re-exported from -@racketmodname[webui-wire-download]; they are documented in more detail -in that module. Briefly: - -@defproc[(ww-set-custom-webui-wire-command! - [cmd string?]) - string?]{ - -Override the command used to start the @tt{webui-wire} helper. Useful in -development or when the helper is installed in a non-standard location. -} - -@defproc[(ww-get-webui-wire-version) string?]{ - -Return the version string reported by the installed @tt{webui-wire} -helper. -} - -@section{Re-exported CSS and menu helpers} - -The module also re-exports: +The following flags are supported: @itemlist[ - @item{@racket[(all-from-out "css.rkt")] — CSS helper functions and - @racket[css-style] values;} - @item{@racket[(all-from-out "menu.rkt")] — menu construction helpers - for building menu structures passed to @racket[ww-webview%].} + @item{@tt{0} — pad numeric output with zeros instead of spaces} + @item{@tt{-} — left-align the result within the field width} ] -See the documentation of those modules for the exact API. +If no flag is specified, numeric values are right-aligned and +padded with spaces. -@section{Element wrapper classes} +@section{Field Width} -These classes provide OO wrappers around DOM elements managed by -@tt{webui-wire}. They work on top of the lower-level @racket[web-wire] -functions that address elements by id. +A minimum field width may be specified as a decimal number: -@subsection{Generic element} +@verbatim{ +%8d +%-10s +} -@defclass[ww-element% object% ()]{ +If the formatted value is shorter than the field width, it is +padded according to the alignment rules. -Base class for element wrappers. +@section{Dynamic Width and Precision} -@defconstructor/auto-super[([win-id ww-win?] - [id (or/c symbol? string?)])] +Both width and precision may be specified using @tt{*}. +In that case, the value is taken from the argument list. -The constructor normally isn’t called directly by user code. Objects are -created automatically by @racket[ww-webview%] when you bind inputs and -buttons or when you explicitly request an element. +Example: -Fields: +@verbatim{ +(sprintf "%*.*f" 8 3 1.23456) +} + +In this example: +@itemlist[ + @item{the first argument specifies the field width} + @item{the second argument specifies the precision} + @item{the third argument is the value to be formatted} +] + +The arguments supplying width or precision must be numbers; +otherwise an error is raised. + +@section{Precision} + +Precision has different meanings depending on the conversion type: @itemlist[ - @item{@racket[win-id] — a @racket[ww-win] handle identifying the WebUI - window that owns this element} - @item{@racket[id] — the element id (symbol or string), as used in the - DOM.} + @item{for numeric conversions, it controls the number of digits after the decimal point} + @item{for strings, it specifies the maximum output width} ] -Public methods: +If no precision is specified: +@itemlist[ + @item{numeric values default to precision 0 for integers} + @item{strings are not truncated} +] -@defmethod[(get-win-id) ww-win?]{ -Return the window handle used for this element.} +@section{Length Modifiers} -@defmethod[(get-id) (or/c symbol? string?)]{ -Return the element id.} +The length modifier field (e.g. @tt{l}) is parsed for syntactic +compatibility but otherwise ignored. +It has no semantic effect. -@defmethod[(win) (or/c ww-webview% #f)]{ -Look up the @racket[ww-webview%] instance for @racket[win-id] in the -global window table. Returns @racket[#f] if the window no longer -exists.} +@section{Type Checking and Errors} -@defmethod[(callback [evt symbol?] [args any/c] ...) any]{ - -Call a previously registered callback for @racket[evt] with the -given @racket[args]. This is used internally when DOM events -arrive from @tt{webui-wire}.} - -@defmethod[(connect [evt symbol?] [func (-> any)]) void?]{ - -Register @racket[func] as the handler for event kind @racket[evt] on -this element.} - -@defmethod[(disconnect [evt symbol?]) void?]{ - -Remove any callback registered for @racket[evt].} - -@defmethod[(add-style! [st css-style?]) void?]{ -Merge the given style into the element’s existing style.} - -@defmethod[(set-style! [st css-style?]) void?]{ -Replace the element’s style with @racket[st].} - -@defmethod[(style) css-style?]{ -Return the current style as a @racket[css-style] value.} - -@defmethod[(get-attr [a (or/c symbol? string?)]) jsexpr?]{ -Get a single attribute value as JSON.} - -@defmethod[(set-attr! [a (or/c symbol? string?)] [val any/c]) void?]{ -Set an attribute value.} - -@defmethod[(del-attr [a (or/c symbol? string?)]) void?]{ -Remove an attribute.} - -@defmethod[(get-attrs) (hash/c symbol? any/c)]{ -Return all attributes as a hash table (symbol → value).} - -@defmethod[(add-class! [cl (or/c symbol? string?)]) void?]{ -Add a CSS class to the element.} - -@defmethod[(remove-class! [cl (or/c symbol? string?)]) void?]{ -Remove a CSS class.} - -@defmethod[(has-class? [cl (or/c symbol? string?)]) boolean?]{ -Test whether the element has a given CSS class.} - -@defmethod[(enable) void?]{ -Remove the @tt{disabled} state from the element -(by manipulating its CSS/attributes).} - -@defmethod[(enabled?) boolean?]{ -Return @racket[#t] when the element is not disabled.} - -@defmethod[(disable) void?]{ -Mark the element as disabled (e.g. by adding a @tt{disabled} attribute -and/or class).} - -@defmethod[(disabled?) boolean?]{ -Return @racket[#t] when the element is disabled.} - -@defmethod[(display [d string? "block"]) void?]{ -Set the CSS @tt{display} style; default is @tt{"block"}.} - -@defmethod[(hide) void?]{ -Convenience for @racket[(display "none")].} - -@defmethod[(show) void?]{ -Show the element again (typically restoring display to a non-@tt{"none"} -value).} - -} - -@subsection{Form input element} - -@defclass[ww-input% ww-element% ()]{ - -Wrapper for input-type elements (fields that have a “value” and that -emit change/input events). Instances are usually created by -@racket[ww-webview%] when you call @racket[bind-inputs]. - -Internally, a @racket[ww-input%] keeps track of: +The formatter performs runtime type checking: @itemlist[ - @item{the current value (mirrored from the DOM);} - @item{an optional change callback.} + @item{numeric conversion specifiers require numeric arguments} + @item{@tt{%s} requires a string argument} ] -Public methods: - -@defmethod[(get) any/c]{ -Return the last known value of the input.} - -@defmethod[(on-change! [callback (-> any/c any)]) void?]{ - -Register a change callback: +Errors are raised in the following situations: @itemlist[ - @item{The callback is stored;} - @item{It is immediately called once with the current value;} - @item{Later, when an @tt{input} event with a @tt{value} field arrives - from the browser, the stored callback is called with the new - value.} -] -} - -@defmethod[(set! [v any/c]) void?]{ - -Set the input’s value both locally and in the DOM. This will normally -update the form control on screen and may also trigger change/input -events in the browser. -} - -@defmethod[(disable) void?]{Disable the input (overrides base behaviour).} - -@defmethod[(enable) void?]{Enable the input again (overrides base -behaviour).} -} - -@section{Webview windows} - -@subsection{Main windows} - -@defclass[ww-webview% object% ()]{ - -Represents a WebUI window driven by the @tt{webui-wire} backend. - -@defconstructor/auto-super[ - ([profile symbol? 'default-profile] - [settings (or/c #f ww-settings%) #f] - [use-browser boolean? #f] - [parent-id (or/c #f ww-win?) #f] - [parent (or/c #f ww-webview%) #f] - [title string? "Racket HTML Window"] - [x number?] - [y number?] - [width number?] - [height number?] - [icon (or/c #f path? string?) #f] - [menu any/c #f] - [html-file (or/c #f path? string?) #f]) -]{ - -The numeric geometry defaults (@racket[x], @racket[y], @racket[width], -@racket[height]) are taken either from @racket[settings] or from builtin -defaults. - -If @racket[parent] or @racket[parent-id] is provided, a child window -is created and positioned relative to the parent; otherwise a normal -top-level window is created. - -If @racket[menu], @racket[icon], or @racket[html-file] are provided, -they are applied after window creation. -} - -Important behaviour during construction: - -@itemlist[ - @item{when the first window is created, @racket[ww-start] is called;} - @item{the new window is registered in the global @racket[windows] - table;} - @item{an event handler is installed so that incoming events from - @tt{webui-wire} are dispatched to methods like - @racket[handle-click] and @racket[handle-change];} - @item{when the last window is destroyed, @racket[ww-stop] is called.} + @item{arguments remain but no format specifiers are left} + @item{format specifiers remain but no arguments are left} + @item{@tt{*} is used but no corresponding numeric argument is available} + @item{argument types do not match the conversion specifier} ] -Key public methods (grouped by responsibility): +Error messages are designed to be explicit and descriptive. -@bold{Settings and cloning} +@section{Implementation Notes} -@defmethod[(clone-settings [section symbol?]) (or/c #f ww-settings%)]{ +Internally, the formatter uses regular-expression driven parsing +and delegates the actual formatting to @racket[~r] and @racket[~a] +from @racket[racket/format]. -If @racket[settings] is non-@racket[#f], call @racket[clone] on it -to obtain a section-specific settings object, otherwise return -@racket[#f]. -} +The implementation is intentionally strict: mismatches between +format strings and arguments are treated as programming errors +and reported immediately. -@bold{Event handling hooks} - -These are normally invoked internally when events arrive from -@tt{webui-wire}, but can be overridden in subclasses. - -@defmethod[(handle-click [element-id symbol?] [data jsexpr?]) void?]{ -Find the element object for @racket[element-id] (if present) and call -its @racket[callback] method with the @racket['click] event.} - -@defmethod[(handle-change [element-id symbol?] [data jsexpr?]) void?]{ -Find the element object and delegate a @racket['change] event.} - -@defmethod[(handle-input [element-id symbol?] [data jsexpr?]) void?]{ -Find the element object and delegate an @tt{input} event.} - -@defmethod[(handle-navigate [url string?] - [type symbol?] - [kind symbol?]) - void?]{ - -Handle navigation events (for example link clicks) as reported by -@tt{webui-wire}. The default implementation may call -@racket[send-url] depending on @racket[type] and @racket[kind].} - -@bold{Element management} - -@defmethod[(get-win-id) ww-win?]{ -Return the underlying @racket[ww-win] handle.} - -@defmethod[(element [id (or/c symbol? string?)]) ww-element%]{ - -Return (and lazily create) an element wrapper for a given DOM id. -The concrete class is chosen based on the element’s tag and type -(@racket[ww-input%] for input fields, or @racket[ww-element%] -otherwise). -} - -@defmethod[(get-elements [selector selector?]) - (listof ww-element%)]{ - -Query elements matching @racket[selector] via @racket[ww-get-elements] -and return a list of element wrapper objects. -} - -@defmethod[(bind [event symbol?] - [selector selector?] - [forced-cl (or/c #f (is-a?/c ww-element%)) #f]) - void?]{ - -Bind @racket[event] to all elements matching @racket[selector]. - -A suitable element class is chosen automatically based on tag/type, -unless @racket[forced-cl] is provided; in that case the given class -is used for all matched elements. -} - -@defmethod[(bind-inputs) boolean?]{ -Convenience: bind @racket['change] events for @tt{input} and -@tt{textarea} elements and create @racket[ww-input%] wrappers.} - -@defmethod[(bind-buttons) void?]{ -Convenience: bind @racket['click] events for @tt{button} elements.} - -@bold{Window geometry and title} - -@defmethod[(move [x number?] [y number?]) void?]{ -Move the window to (@racket[x], @racket[y]).} - -@defmethod[(resize [x number?] [y number?]) void?]{ -Resize the window to width @racket[x], height @racket[y].} - -@defmethod[(get-x) number?]{Current x position (cached).} -@defmethod[(get-y) number?]{Current y position (cached).} -@defmethod[(get-width) number?]{Current width (cached).} -@defmethod[(get-height) number?]{Current height (cached).} - -@defmethod[(geom) (list number? number? number? number?)]{ -Return @racket[(list x y width height)].} - -@defmethod[(set-title! [t string?]) void?]{ -Set the window title (cached locally and sent to the backend).} - -@defmethod[(get-title) string?]{ -Return the last title set.} - -@bold{Show/hide and lifetime} - -@defmethod[(show) void?]{ -Show the window (set show state to @racket['show]).} - -@defmethod[(hide) void?]{ -Hide the window.} - -@defmethod[(maximize) void?]{ -Maximise the window.} - -@defmethod[(normalize) void?]{ -Restore the window to its normal state.} - -@defmethod[(minimize) void?]{ -Minimise the window.} - -@defmethod[(fullscreen) void?]{ -Request a fullscreen display (if supported).} - -@defmethod[(show-state) symbol?]{ -Return the current show state as reported by the backend.} - -@defmethod[(can-close?) boolean?]{ -Return @racket[#t] if the window may be closed; subclasses can -override this to veto close requests.} - -@defmethod[(close) void?]{ - -Close the window, unregister it from the global tables, and if this is -the last window, stop the backend by calling @racket[ww-stop]. -} - -@bold{Menus} - -@defmethod[(set-menu! [menu-def is-menu?]) void?]{ -Set the window menu using a structure created with the helpers from -@racket["menu.rkt"].} - -@defmethod[(connect-menu! [id symbol?] [cb (-> any)]) void?]{ - -Associate a callback @racket[cb] with a menu item id. When the menu -item is activated, the callback is called. -} - -@defmethod[(popup-menu [menu-def is-menu?]) void?]{ -Pops up a contextmenu at the current cursor position. -The menu is created using a structure created with the helpers from -@racket["menu.rkt"].} - - -@bold{HTML and navigation} - -@defmethod[(set-icon! [icn (or/c path? string?)]) void?]{ -Set the window icon; the file is validated by @racket[ww-set-icon].} - -@defmethod[(set-html-file! [file (or/c path? string?)]) void?]{ - -Load the given HTML file. The directory part is used to adjust -@racket[ww-cwd], and the file name is passed to -@racket[ww-set-html-file].} - -@defmethod[(set-html [html string?]) void?]{ - -Write @racket[html] to a temporary file and call -@racket[set-html-file!] with it. Intended for simple dynamic content.} - -@defmethod[(set-url [url string?]) void?]{ -Open the given URL using @racket[send-url].} - -@defmethod[(html-loaded) void?]{ - -Hook that is called when a page is fully loaded and matches the -current HTML handle. The default implementation binds buttons and -inputs by calling @racket[bind-buttons] and @racket[bind-inputs]. -} - -@bold{File and directory dialogs} - -@defmethod[(file-open [caption string?] - [base-dir string?] - [filters string?]) - (or/c #f path?)]{ - -Show a file-open dialog and return the selected path as a @racket[path], -or @racket[#f] if the operation failed or was cancelled. -} - -@defmethod[(file-save [caption string?] - [base-dir string?] - [filters string?] - [overwrite boolean? #f]) - (or/c #f path?)]{ - -Show a file-save dialog. The @racket[overwrite] flag controls whether -overwriting existing files is allowed. Returns the chosen path, or -@racket[#f] if cancelled or if the command failed. -} - -@defmethod[(choose-dir [caption string?] - [base-dir string?]) - (or/c #f path?)]{ - -Show a directory chooser dialog; return the chosen directory, or -@racket[#f] on cancel/failure. -} - -@bold{Hook for subclasses} - -@defmethod[(inherit-checks) boolean?]{ - -Called early during construction to allow subclasses to enforce -preconditions (for example: “dialog windows must have a parent”). -The default implementation simply returns @racket[#t]. -} -} - - - - -@subsection{Dialog windows} - -@defclass[ww-webview-dialog% ww-webview% ()]{ - -Subclass of @racket[ww-webview%] representing dialog-style windows. - -The constructor is the same as @racket[ww-webview%], but -@racket[inherit-checks] is overridden to enforce that a @racket[parent] -was supplied: - -@racketblock[ - (define/override (inherit-checks) - (when (eq? parent #f) - (error "A parent must be given"))) -] - -So, to create a dialog, always pass a parent window: - -@racketblock[ - (define main (new ww-webview%)) - (define dlg (new ww-webview-dialog% [parent main])) -] -} - - - -@subsection{Message dialogs} - -@defclass[ww-webview-message% ww-webview-dialog% ()]{ - -Simple message-dialog subclass. - -The constructor: - -@itemlist[ - @item{calls the @racket[ww-webview-dialog%] constructor;} - @item{sets a minimal HTML page containing a message header and a - submessage, with known element ids (e.g. @tt{"msg"} and - @tt{"submsg"}).} -] - -You can then obtain element wrappers for those ids via -@racket[send] @racket[this] @racket[element] and update their inner -HTML or text using the usual element methods. -} - - - -@section{Settings abstraction} - -@defclass[ww-settings% object% ()]{ - -Abstract base class for storing and retrieving configuration values -used by @racket[ww-webview%] (for example window geometry). - -The default implementation only signals errors; you are expected to -subclass @racket[ww-settings%] and override the methods. - -Public methods: - -@defmethod[(set [key symbol?] [value any/c]) void?]{ -Set a configuration value for @racket[key]. Default implementation -raises an error.} - -@defmethod[(get [key symbol?] [default any/c]) any/c]{ - -Look up a configuration value for @racket[key]. If the key is not -present, return @racket[default] (if provided) or raise an error. The -default implementation always raises an error. -} - -@defmethod[(clone [new-section symbol?]) ww-settings%]{ - -Return a “cloned” settings object for a given section or profile. -Used by @racket[ww-webview%] to derive per-window settings from a -shared base. Default implementation raises an error. -} - -@defmethod[(set! [key symbol?] [value any/c]) void?]{ - -Convenience that forwards to @racket[set]. Provided for symmetry with -Racket’s @tt{set!} naming style. -} - - -}