diff --git a/core/block_svg.ts b/core/block_svg.ts index a30cc34ed..49b4a1ee6 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -299,8 +299,19 @@ export class BlockSvg } const oldXY = this.getRelativeToSurfaceXY(); + const focusedNode = getFocusManager().getFocusedNode(); + const restoreFocus = this.getSvgRoot().contains( + focusedNode?.getFocusableElement() ?? null, + ); if (newParent) { (newParent as BlockSvg).getSvgRoot().appendChild(svgRoot); + // appendChild() clears focus state, so re-focus the previously focused + // node in case it was this block and would otherwise lose its focus. Once + // Element.moveBefore() has better browser support, it should be used + // instead. + if (restoreFocus && focusedNode) { + getFocusManager().focusNode(focusedNode); + } } else if (oldParent) { // If we are losing a parent, we want to move our DOM element to the // root of the workspace. Try to insert it before any top-level @@ -319,6 +330,13 @@ export class BlockSvg canvas.insertBefore(svgRoot, draggingBlockElement); } else { canvas.appendChild(svgRoot); + // appendChild() clears focus state, so re-focus the previously focused + // node in case it was this block and would otherwise lose its focus. Once + // Element.moveBefore() has better browser support, it should be used + // instead. + if (restoreFocus && focusedNode) { + getFocusManager().focusNode(focusedNode); + } } this.translate(oldXY.x, oldXY.y); } @@ -849,10 +867,30 @@ export class BlockSvg Tooltip.dispose(); ContextMenu.hide(); - // If this block was focused, focus its parent or workspace instead. + // If this block (or a descendant) was focused, focus its parent or + // workspace instead. const focusManager = getFocusManager(); - if (focusManager.getFocusedNode() === this) { - const parent = this.getParent(); + if ( + this.getSvgRoot().contains( + focusManager.getFocusedNode()?.getFocusableElement() ?? null, + ) + ) { + let parent: BlockSvg | undefined | null = this.getParent(); + if (!parent) { + // In some cases, blocks are disconnected from their parents before + // being deleted. Attempt to infer if there was a parent by checking + // for a connection within a radius of 0. Even if this wasn't a parent, + // it must be adjacent to this block and so is as good an option as any + // to focus after deleting. + const connection = this.outputConnection ?? this.previousConnection; + if (connection) { + const targetConnection = connection.closest( + 0, + new Coordinate(0, 0), + ).connection; + parent = targetConnection?.getSourceBlock(); + } + } if (parent) { focusManager.focusNode(parent); } else { diff --git a/core/comments.ts b/core/comments.ts index 86e8f50b9..179ab4a33 100644 --- a/core/comments.ts +++ b/core/comments.ts @@ -4,7 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +export {CollapseCommentBarButton} from './comments/collapse_comment_bar_button.js'; +export {CommentBarButton} from './comments/comment_bar_button.js'; export {CommentEditor} from './comments/comment_editor.js'; export {CommentView} from './comments/comment_view.js'; +export {DeleteCommentBarButton} from './comments/delete_comment_bar_button.js'; export {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; export {WorkspaceComment} from './comments/workspace_comment.js'; diff --git a/core/comments/collapse_comment_bar_button.ts b/core/comments/collapse_comment_bar_button.ts new file mode 100644 index 000000000..b0738d707 --- /dev/null +++ b/core/comments/collapse_comment_bar_button.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as browserEvents from '../browser_events.js'; +import * as touch from '../touch.js'; +import * as dom from '../utils/dom.js'; +import {Svg} from '../utils/svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import {CommentBarButton} from './comment_bar_button.js'; + +/** + * Magic string appended to the comment ID to create a unique ID for this button. + */ +export const COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER = + '_collapse_bar_button'; + +/** + * Button that toggles the collapsed state of a comment. + */ +export class CollapseCommentBarButton extends CommentBarButton { + /** + * Opaque ID used to unbind event handlers during disposal. + */ + private readonly bindId: browserEvents.Data; + + /** + * SVG image displayed on this button. + */ + protected override readonly icon: SVGImageElement; + + /** + * Creates a new CollapseCommentBarButton instance. + * + * @param id The ID of this button's parent comment. + * @param workspace The workspace this button's parent comment is displayed on. + * @param container An SVG group that this button should be a child of. + */ + constructor( + protected readonly id: string, + protected readonly workspace: WorkspaceSvg, + protected readonly container: SVGGElement, + ) { + super(id, workspace, container); + + this.icon = dom.createSvgElement( + Svg.IMAGE, + { + 'class': 'blocklyFoldoutIcon', + 'href': `${this.workspace.options.pathToMedia}foldout-icon.svg`, + 'id': `${this.id}${COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER}`, + }, + this.container, + ); + this.bindId = browserEvents.conditionalBind( + this.icon, + 'pointerdown', + this, + this.performAction.bind(this), + ); + } + + /** + * Disposes of this button. + */ + dispose() { + browserEvents.unbind(this.bindId); + } + + /** + * Adjusts the positioning of this button within its container. + */ + override reposition() { + const margin = this.getMargin(); + this.icon.setAttribute('y', `${margin}`); + this.icon.setAttribute('x', `${margin}`); + } + + /** + * Toggles the collapsed state of the parent comment. + * + * @param e The event that triggered this action. + */ + override performAction(e?: Event) { + touch.clearTouchIdentifier(); + + const comment = this.getParentComment(); + comment.view.bringToFront(); + if (e && e instanceof PointerEvent && browserEvents.isRightButton(e)) { + e.stopPropagation(); + return; + } + + comment.setCollapsed(!comment.isCollapsed()); + this.workspace.hideChaff(); + + e?.stopPropagation(); + } +} diff --git a/core/comments/comment_bar_button.ts b/core/comments/comment_bar_button.ts new file mode 100644 index 000000000..d78a7fd86 --- /dev/null +++ b/core/comments/comment_bar_button.ts @@ -0,0 +1,105 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import {Rect} from '../utils/rect.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import type {RenderedWorkspaceComment} from './rendered_workspace_comment.js'; + +/** + * Button displayed on a comment's top bar. + */ +export abstract class CommentBarButton implements IFocusableNode { + /** + * SVG image displayed on this button. + */ + protected abstract readonly icon: SVGImageElement; + + /** + * Creates a new CommentBarButton instance. + * + * @param id The ID of this button's parent comment. + * @param workspace The workspace this button's parent comment is on. + * @param container An SVG group that this button should be a child of. + */ + constructor( + protected readonly id: string, + protected readonly workspace: WorkspaceSvg, + protected readonly container: SVGGElement, + ) {} + + /** + * Returns whether or not this button is currently visible. + */ + isVisible(): boolean { + return this.icon.checkVisibility(); + } + + /** + * Returns the parent comment of this comment bar button. + */ + getParentComment(): RenderedWorkspaceComment { + const comment = this.workspace.getCommentById(this.id); + if (!comment) { + throw new Error( + `Comment bar button ${this.id} has no corresponding comment`, + ); + } + + return comment; + } + + /** Adjusts the position of this button within its parent container. */ + abstract reposition(): void; + + /** Perform the action this button should take when it is acted on. */ + abstract performAction(e?: Event): void; + + /** + * Returns the dimensions of this button in workspace coordinates. + * + * @param includeMargin True to include the margin when calculating the size. + * @returns The size of this button. + */ + getSize(includeMargin = false): Rect { + const bounds = this.icon.getBBox(); + const rect = Rect.from(bounds); + if (includeMargin) { + const margin = this.getMargin(); + rect.left -= margin; + rect.top -= margin; + rect.bottom += margin; + rect.right += margin; + } + return rect; + } + + /** Returns the margin in workspace coordinates surrounding this button. */ + getMargin(): number { + return (this.container.getBBox().height - this.icon.getBBox().height) / 2; + } + + /** Returns a DOM element representing this button that can receive focus. */ + getFocusableElement() { + return this.icon; + } + + /** Returns the workspace this button is a child of. */ + getFocusableTree() { + return this.workspace; + } + + /** Called when this button's focusable DOM element gains focus. */ + onNodeFocus() {} + + /** Called when this button's focusable DOM element loses focus. */ + onNodeBlur() {} + + /** Returns whether this button can be focused. True if it is visible. */ + canBeFocused() { + return this.isVisible(); + } +} diff --git a/core/comments/comment_view.ts b/core/comments/comment_view.ts index 1e5ad4a52..936d74650 100644 --- a/core/comments/comment_view.ts +++ b/core/comments/comment_view.ts @@ -16,14 +16,17 @@ import * as drag from '../utils/drag.js'; import {Size} from '../utils/size.js'; import {Svg} from '../utils/svg.js'; import {WorkspaceSvg} from '../workspace_svg.js'; +import {CollapseCommentBarButton} from './collapse_comment_bar_button.js'; +import {CommentBarButton} from './comment_bar_button.js'; import {CommentEditor} from './comment_editor.js'; +import {DeleteCommentBarButton} from './delete_comment_bar_button.js'; export class CommentView implements IRenderedElement { /** The root group element of the comment view. */ private svgRoot: SVGGElement; /** - * The svg rect element that we use to create a hightlight around the comment. + * The SVG rect element that we use to create a highlight around the comment. */ private highlightRect: SVGRectElement; @@ -33,11 +36,11 @@ export class CommentView implements IRenderedElement { /** The rect background for the top bar. */ private topBarBackground: SVGRectElement; - /** The delete icon that goes in the top bar. */ - private deleteIcon: SVGImageElement; + /** The delete button that goes in the top bar. */ + private deleteButton: DeleteCommentBarButton; - /** The foldout icon that goes in the top bar. */ - private foldoutIcon: SVGImageElement; + /** The foldout button that goes in the top bar. */ + private foldoutButton: CollapseCommentBarButton; /** The text element that goes in the top bar. */ private textPreview: SVGTextElement; @@ -99,7 +102,7 @@ export class CommentView implements IRenderedElement { constructor( readonly workspace: WorkspaceSvg, - private commentId?: string, + private commentId: string, ) { this.svgRoot = dom.createSvgElement(Svg.G, { 'class': 'blocklyComment blocklyEditable blocklyDraggable', @@ -110,11 +113,11 @@ export class CommentView implements IRenderedElement { ({ topBarGroup: this.topBarGroup, topBarBackground: this.topBarBackground, - deleteIcon: this.deleteIcon, - foldoutIcon: this.foldoutIcon, + deleteButton: this.deleteButton, + foldoutButton: this.foldoutButton, textPreview: this.textPreview, textPreviewNode: this.textPreviewNode, - } = this.createTopBar(this.svgRoot, workspace)); + } = this.createTopBar(this.svgRoot)); this.commentEditor = this.createTextArea(); @@ -147,14 +150,11 @@ export class CommentView implements IRenderedElement { * Creates the top bar and the elements visually within it. * Registers event listeners. */ - private createTopBar( - svgRoot: SVGGElement, - workspace: WorkspaceSvg, - ): { + private createTopBar(svgRoot: SVGGElement): { topBarGroup: SVGGElement; topBarBackground: SVGRectElement; - deleteIcon: SVGImageElement; - foldoutIcon: SVGImageElement; + deleteButton: DeleteCommentBarButton; + foldoutButton: CollapseCommentBarButton; textPreview: SVGTextElement; textPreviewNode: Text; } { @@ -172,22 +172,14 @@ export class CommentView implements IRenderedElement { }, topBarGroup, ); - // TODO: Before merging, does this mean to override an individual image, - // folks need to replace the whole media folder? - const deleteIcon = dom.createSvgElement( - Svg.IMAGE, - { - 'class': 'blocklyDeleteIcon', - 'href': `${workspace.options.pathToMedia}delete-icon.svg`, - }, + const deleteButton = new DeleteCommentBarButton( + this.commentId, + this.workspace, topBarGroup, ); - const foldoutIcon = dom.createSvgElement( - Svg.IMAGE, - { - 'class': 'blocklyFoldoutIcon', - 'href': `${workspace.options.pathToMedia}foldout-icon.svg`, - }, + const foldoutButton = new CollapseCommentBarButton( + this.commentId, + this.workspace, topBarGroup, ); const textPreview = dom.createSvgElement( @@ -200,27 +192,11 @@ export class CommentView implements IRenderedElement { const textPreviewNode = document.createTextNode(''); textPreview.appendChild(textPreviewNode); - // TODO(toychest): Triggering this on pointerdown means that we can't start - // drags on the foldout icon. We need to open up the gesture system - // to fix this. - browserEvents.conditionalBind( - foldoutIcon, - 'pointerdown', - this, - this.onFoldoutDown, - ); - browserEvents.conditionalBind( - deleteIcon, - 'pointerdown', - this, - this.onDeleteDown, - ); - return { topBarGroup, topBarBackground, - deleteIcon, - foldoutIcon, + deleteButton, + foldoutButton, textPreview, textPreviewNode, }; @@ -300,15 +276,10 @@ export class CommentView implements IRenderedElement { */ setSizeWithoutFiringEvents(size: Size) { const topBarSize = this.topBarBackground.getBBox(); - const deleteSize = this.deleteIcon.getBBox(); - const foldoutSize = this.foldoutIcon.getBBox(); const textPreviewSize = this.textPreview.getBBox(); const resizeSize = this.resizeHandle.getBBox(); - size = Size.max( - size, - this.calcMinSize(topBarSize, foldoutSize, deleteSize), - ); + size = Size.max(size, this.calcMinSize(topBarSize)); this.size = size; this.svgRoot.setAttribute('height', `${size.height}`); @@ -317,15 +288,9 @@ export class CommentView implements IRenderedElement { this.updateHighlightRect(size); this.updateTopBarSize(size); this.commentEditor.updateSize(size, topBarSize); - this.updateDeleteIconPosition(size, topBarSize, deleteSize); - this.updateFoldoutIconPosition(topBarSize, foldoutSize); - this.updateTextPreviewSize( - size, - topBarSize, - textPreviewSize, - deleteSize, - resizeSize, - ); + this.deleteButton.reposition(); + this.foldoutButton.reposition(); + this.updateTextPreviewSize(size, topBarSize, textPreviewSize); this.updateResizeHandlePosition(size, resizeSize); } @@ -347,25 +312,18 @@ export class CommentView implements IRenderedElement { * * The minimum height is based on the height of the top bar. */ - private calcMinSize( - topBarSize: Size, - foldoutSize: Size, - deleteSize: Size, - ): Size { + private calcMinSize(topBarSize: Size): Size { this.updateTextPreview(this.commentEditor.getText() ?? ''); const textPreviewWidth = dom.getTextWidth(this.textPreview); - const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize); - const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize); - let width = textPreviewWidth; - if (this.foldoutIcon.checkVisibility()) { - width += foldoutSize.width + foldoutMargin * 2; + if (this.foldoutButton.isVisible()) { + width += this.foldoutButton.getSize(true).getWidth(); } else if (textPreviewWidth) { width += 4; // Arbitrary margin before text. } - if (this.deleteIcon.checkVisibility()) { - width += deleteSize.width + deleteMargin * 2; + if (this.deleteButton.isVisible()) { + width += this.deleteButton.getSize(true).getWidth(); } else if (textPreviewWidth) { width += 4; // Arbitrary margin after text. } @@ -376,16 +334,6 @@ export class CommentView implements IRenderedElement { return new Size(width, height); } - /** Calculates the margin that should exist around the delete icon. */ - private calcDeleteMargin(topBarSize: Size, deleteSize: Size) { - return (topBarSize.height - deleteSize.height) / 2; - } - - /** Calculates the margin that should exist around the foldout icon. */ - private calcFoldoutMargin(topBarSize: Size, foldoutSize: Size) { - return (topBarSize.height - foldoutSize.height) / 2; - } - /** Updates the size of the highlight rect to reflect the new size. */ private updateHighlightRect(size: Size) { this.highlightRect.setAttribute('height', `${size.height}`); @@ -400,31 +348,6 @@ export class CommentView implements IRenderedElement { this.topBarBackground.setAttribute('width', `${size.width}`); } - /** - * Updates the position of the delete icon elements to reflect the new size. - */ - private updateDeleteIconPosition( - size: Size, - topBarSize: Size, - deleteSize: Size, - ) { - const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize); - this.deleteIcon.setAttribute('y', `${deleteMargin}`); - this.deleteIcon.setAttribute( - 'x', - `${size.width - deleteSize.width - deleteMargin}`, - ); - } - - /** - * Updates the position of the foldout icon elements to reflect the new size. - */ - private updateFoldoutIconPosition(topBarSize: Size, foldoutSize: Size) { - const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize); - this.foldoutIcon.setAttribute('y', `${foldoutMargin}`); - this.foldoutIcon.setAttribute('x', `${foldoutMargin}`); - } - /** * Updates the size and position of the text preview elements to reflect the new size. */ @@ -432,25 +355,14 @@ export class CommentView implements IRenderedElement { size: Size, topBarSize: Size, textPreviewSize: Size, - deleteSize: Size, - foldoutSize: Size, ) { const textPreviewMargin = (topBarSize.height - textPreviewSize.height) / 2; - const deleteMargin = this.calcDeleteMargin(topBarSize, deleteSize); - const foldoutMargin = this.calcFoldoutMargin(topBarSize, foldoutSize); + const foldoutSize = this.foldoutButton.getSize(true); + const deleteSize = this.deleteButton.getSize(true); const textPreviewWidth = - size.width - - foldoutSize.width - - foldoutMargin * 2 - - deleteSize.width - - deleteMargin * 2; - this.textPreview.setAttribute( - 'x', - `${ - foldoutSize.width + foldoutMargin * 2 * (this.workspace.RTL ? -1 : 1) - }`, - ); + size.width - foldoutSize.getWidth() - deleteSize.getWidth(); + this.textPreview.setAttribute('x', `${foldoutSize.getWidth()}`); this.textPreview.setAttribute( 'y', `${textPreviewMargin + textPreviewSize.height / 2}`, @@ -601,25 +513,6 @@ export class CommentView implements IRenderedElement { ); } - /** - * Toggles the collapsedness of the block when we receive a pointer down - * event on the foldout icon. - */ - private onFoldoutDown(e: PointerEvent) { - touch.clearTouchIdentifier(); - this.bringToFront(); - if (browserEvents.isRightButton(e)) { - e.stopPropagation(); - return; - } - - this.setCollapsed(!this.collapsed); - - this.workspace.hideChaff(); - - e.stopPropagation(); - } - /** Returns true if the comment is currently editable. */ isEditable(): boolean { return this.editable; @@ -692,7 +585,7 @@ export class CommentView implements IRenderedElement { } /** Brings the workspace comment to the front of its layer. */ - private bringToFront() { + bringToFront() { const parent = this.svgRoot.parentNode; const childNodes = parent!.childNodes; // Avoid moving the comment if it's already at the bottom. @@ -719,6 +612,8 @@ export class CommentView implements IRenderedElement { /** Disposes of this comment view. */ dispose() { this.disposing = true; + this.foldoutButton.dispose(); + this.deleteButton.dispose(); dom.removeNode(this.svgRoot); // Loop through listeners backwards in case they remove themselves. for (let i = this.disposeListeners.length - 1; i >= 0; i--) { @@ -749,6 +644,13 @@ export class CommentView implements IRenderedElement { removeDisposeListener(listener: () => void) { this.disposeListeners.splice(this.disposeListeners.indexOf(listener), 1); } + + /** + * @internal + */ + getCommentBarButtons(): CommentBarButton[] { + return [this.foldoutButton, this.deleteButton]; + } } css.register(` diff --git a/core/comments/delete_comment_bar_button.ts b/core/comments/delete_comment_bar_button.ts new file mode 100644 index 000000000..ccdd02539 --- /dev/null +++ b/core/comments/delete_comment_bar_button.ts @@ -0,0 +1,102 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as browserEvents from '../browser_events.js'; +import * as touch from '../touch.js'; +import * as dom from '../utils/dom.js'; +import {Svg} from '../utils/svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import {CommentBarButton} from './comment_bar_button.js'; + +/** + * Magic string appended to the comment ID to create a unique ID for this button. + */ +export const COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER = '_delete_bar_button'; + +/** + * Button that deletes a comment. + */ +export class DeleteCommentBarButton extends CommentBarButton { + /** + * Opaque ID used to unbind event handlers during disposal. + */ + private readonly bindId: browserEvents.Data; + + /** + * SVG image displayed on this button. + */ + protected override readonly icon: SVGImageElement; + + /** + * Creates a new DeleteCommentBarButton instance. + * + * @param id The ID of this button's parent comment. + * @param workspace The workspace this button's parent comment is shown on. + * @param container An SVG group that this button should be a child of. + */ + constructor( + protected readonly id: string, + protected readonly workspace: WorkspaceSvg, + protected readonly container: SVGGElement, + ) { + super(id, workspace, container); + + this.icon = dom.createSvgElement( + Svg.IMAGE, + { + 'class': 'blocklyDeleteIcon', + 'href': `${this.workspace.options.pathToMedia}delete-icon.svg`, + 'id': `${this.id}${COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER}`, + }, + container, + ); + this.bindId = browserEvents.conditionalBind( + this.icon, + 'pointerdown', + this, + this.performAction.bind(this), + ); + } + + /** + * Disposes of this button. + */ + dispose() { + browserEvents.unbind(this.bindId); + } + + /** + * Adjusts the positioning of this button within its container. + */ + override reposition() { + const margin = this.getMargin(); + // Reset to 0 so that our position doesn't force the parent container to + // grow. + this.icon.setAttribute('x', `0`); + const containerSize = this.container.getBBox(); + this.icon.setAttribute('y', `${margin}`); + this.icon.setAttribute( + 'x', + `${containerSize.width - this.getSize(true).getWidth()}`, + ); + } + + /** + * Deletes parent comment. + * + * @param e The event that triggered this action. + */ + override performAction(e?: Event) { + touch.clearTouchIdentifier(); + if (e && e instanceof PointerEvent && browserEvents.isRightButton(e)) { + e.stopPropagation(); + return; + } + + this.getParentComment().dispose(); + e?.stopPropagation(); + } +} diff --git a/core/connection.ts b/core/connection.ts index fbd094dba..a79b7b9b1 100644 --- a/core/connection.ts +++ b/core/connection.ts @@ -83,6 +83,12 @@ export class Connection { public type: number, ) { this.sourceBlock_ = source; + if (source.id.includes('_connection')) { + throw new Error( + `Connection ID indicator is contained in block ID. This will cause ` + + `problems with focus: ${source.id}.`, + ); + } this.id = `${source.id}_connection_${idGenerator.getNextUniqueId()}`; } diff --git a/core/field.ts b/core/field.ts index c4b651478..fdcb2d693 100644 --- a/core/field.ts +++ b/core/field.ts @@ -265,6 +265,12 @@ export abstract class Field throw Error('Field already bound to a block'); } this.sourceBlock_ = block; + if (block.id.includes('_field')) { + throw new Error( + `Field ID indicator is contained in block ID. This may cause ` + + `problems with focus: ${block.id}.`, + ); + } this.id_ = `${block.id}_field_${idGenerator.getNextUniqueId()}`; } diff --git a/core/field_input.ts b/core/field_input.ts index c7921d6f0..b68530918 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -27,6 +27,7 @@ import { FieldValidator, UnattachedFieldError, } from './field.js'; +import {getFocusManager} from './focus_manager.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import {Msg} from './msg.js'; import * as renderManagement from './render_management.js'; @@ -83,8 +84,8 @@ export abstract class FieldInput extends Field< /** Key down event data. */ private onKeyDownWrapper: browserEvents.Data | null = null; - /** Key input event data. */ - private onKeyInputWrapper: browserEvents.Data | null = null; + /** Input element input event data. */ + private onInputWrapper: browserEvents.Data | null = null; /** * Whether the field should consider the whole parent block to be its click @@ -558,7 +559,7 @@ export abstract class FieldInput extends Field< this.onHtmlInputKeyDown_, ); // Resize after every input change. - this.onKeyInputWrapper = browserEvents.conditionalBind( + this.onInputWrapper = browserEvents.conditionalBind( htmlInput, 'input', this, @@ -572,9 +573,9 @@ export abstract class FieldInput extends Field< browserEvents.unbind(this.onKeyDownWrapper); this.onKeyDownWrapper = null; } - if (this.onKeyInputWrapper) { - browserEvents.unbind(this.onKeyInputWrapper); - this.onKeyInputWrapper = null; + if (this.onInputWrapper) { + browserEvents.unbind(this.onInputWrapper); + this.onInputWrapper = null; } } @@ -614,6 +615,14 @@ export abstract class FieldInput extends Field< if (target instanceof FieldInput) { WidgetDiv.hideIfOwner(this); dropDownDiv.hideWithoutAnimation(); + const targetSourceBlock = target.getSourceBlock(); + if ( + target.isFullBlockField() && + targetSourceBlock && + targetSourceBlock instanceof BlockSvg + ) { + getFocusManager().focusNode(targetSourceBlock); + } else getFocusManager().focusNode(target); target.showEditor(); } } @@ -622,7 +631,7 @@ export abstract class FieldInput extends Field< /** * Handle a change to the editor. * - * @param _e Keyboard event. + * @param _e InputEvent. */ private onHtmlInputChange(_e: Event) { // Intermediate value changes from user input are not confirmed until the diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 3d0a9347f..02e059107 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -174,8 +174,15 @@ export class FocusManager { this.registeredTrees.push( new TreeRegistration(tree, rootShouldBeAutoTabbable), ); + const rootElement = tree.getRootFocusableNode().getFocusableElement(); + if (!rootElement.id || rootElement.id === 'null') { + throw Error( + `Attempting to register a tree with a root element that has an ` + + `invalid ID: ${tree}.`, + ); + } if (rootShouldBeAutoTabbable) { - tree.getRootFocusableNode().getFocusableElement().tabIndex = 0; + rootElement.tabIndex = 0; } } @@ -344,13 +351,22 @@ export class FocusManager { throw Error(`Attempted to focus unregistered node: ${focusableNode}.`); } + const focusableNodeElement = focusableNode.getFocusableElement(); + if (!focusableNodeElement.id || focusableNodeElement.id === 'null') { + // Warn that the ID is invalid, but continue execution since an invalid ID + // will result in an unmatched (null) node. Since a request to focus + // something was initiated, the code below will attempt to find the next + // best thing to focus, instead. + console.warn('Trying to focus a node that has an invalid ID.'); + } + // Safety check for ensuring focusNode() doesn't get called for a node that // isn't actually hooked up to its parent tree correctly. This usually // happens when calls to focusNode() interleave with asynchronous clean-up // operations (which can happen due to ephemeral focus and in other cases). // Fall back to a reasonable default since there's no valid node to focus. const matchedNode = FocusableTreeTraverser.findFocusableNodeFor( - focusableNode.getFocusableElement(), + focusableNodeElement, nextTree, ); const prevNodeNextTree = FocusableTreeTraverser.findFocusedNode(nextTree); diff --git a/core/keyboard_nav/block_navigation_policy.ts b/core/keyboard_nav/block_navigation_policy.ts index 2637ad49d..9f56b5384 100644 --- a/core/keyboard_nav/block_navigation_policy.ts +++ b/core/keyboard_nav/block_navigation_policy.ts @@ -8,8 +8,11 @@ import {BlockSvg} from '../block_svg.js'; import {ConnectionType} from '../connection_type.js'; import type {Field} from '../field.js'; import type {Icon} from '../icons/icon.js'; +import type {IBoundedElement} from '../interfaces/i_bounded_element.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import {isFocusableNode} from '../interfaces/i_focusable_node.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import type {ISelectable} from '../interfaces/i_selectable.js'; import {RenderedConnection} from '../rendered_connection.js'; import {WorkspaceSvg} from '../workspace_svg.js'; @@ -143,21 +146,25 @@ function getBlockNavigationCandidates( } /** - * Returns the next/previous stack relative to the given block's stack. + * Returns the next/previous stack relative to the given element's stack. * - * @param current The block whose stack will be navigated relative to. + * @param current The element whose stack will be navigated relative to. * @param delta The difference in index to navigate; positive values navigate * to the nth next stack, while negative values navigate to the nth previous * stack. - * @returns The first block in the stack offset by `delta` relative to the - * current block's stack, or the last block in the stack offset by `delta` - * relative to the current block's stack when navigating backwards. + * @returns The first element in the stack offset by `delta` relative to the + * current element's stack, or the last element in the stack offset by + * `delta` relative to the current element's stack when navigating backwards. */ -export function navigateStacks(current: BlockSvg, delta: number) { - const stacks = current.workspace.getTopBlocks(true); - const currentIndex = stacks.indexOf(current.getRootBlock()); +export function navigateStacks(current: ISelectable, delta: number) { + const stacks: IFocusableNode[] = (current.workspace as WorkspaceSvg) + .getTopBoundedElements(true) + .filter((element: IBoundedElement) => isFocusableNode(element)); + const currentIndex = stacks.indexOf( + current instanceof BlockSvg ? current.getRootBlock() : current, + ); const targetIndex = currentIndex + delta; - let result: BlockSvg | null = null; + let result: IFocusableNode | null = null; if (targetIndex >= 0 && targetIndex < stacks.length) { result = stacks[targetIndex]; } else if (targetIndex < 0) { @@ -166,9 +173,9 @@ export function navigateStacks(current: BlockSvg, delta: number) { result = stacks[0]; } - // When navigating to a previous stack, our previous sibling is the last + // When navigating to a previous block stack, our previous sibling is the last // block in it. - if (delta < 0 && result) { + if (delta < 0 && result instanceof BlockSvg) { return result.lastConnectionInStack(false)?.getSourceBlock() ?? result; } diff --git a/core/keyboard_nav/comment_bar_button_navigation_policy.ts b/core/keyboard_nav/comment_bar_button_navigation_policy.ts new file mode 100644 index 000000000..f676f4655 --- /dev/null +++ b/core/keyboard_nav/comment_bar_button_navigation_policy.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {CommentBarButton} from '../comments/comment_bar_button.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a CommentBarButton. + */ +export class CommentBarButtonNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given CommentBarButton. + * + * @param _current The CommentBarButton to return the first child of. + * @returns Null. + */ + getFirstChild(_current: CommentBarButton): IFocusableNode | null { + return null; + } + + /** + * Returns the parent of the given CommentBarButton. + * + * @param current The CommentBarButton to return the parent of. + * @returns The parent comment of the given CommentBarButton. + */ + getParent(current: CommentBarButton): IFocusableNode | null { + return current.getParentComment(); + } + + /** + * Returns the next peer node of the given CommentBarButton. + * + * @param current The CommentBarButton to find the following element of. + * @returns The next CommentBarButton, if any. + */ + getNextSibling(current: CommentBarButton): IFocusableNode | null { + const children = current.getParentComment().view.getCommentBarButtons(); + const currentIndex = children.indexOf(current); + if (currentIndex >= 0 && currentIndex + 1 < children.length) { + return children[currentIndex + 1]; + } + return null; + } + + /** + * Returns the previous peer node of the given CommentBarButton. + * + * @param current The CommentBarButton to find the preceding element of. + * @returns The CommentBarButton's previous CommentBarButton, if any. + */ + getPreviousSibling(current: CommentBarButton): IFocusableNode | null { + const children = current.getParentComment().view.getCommentBarButtons(); + const currentIndex = children.indexOf(current); + if (currentIndex > 0) { + return children[currentIndex - 1]; + } + return null; + } + + /** + * Returns whether or not the given CommentBarButton can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given CommentBarButton can be focused. + */ + isNavigable(current: CommentBarButton): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is an CommentBarButton. + */ + isApplicable(current: any): current is CommentBarButton { + return current instanceof CommentBarButton; + } +} diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 89668dedb..aeb80cff1 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -14,6 +14,7 @@ */ import {BlockSvg} from '../block_svg.js'; +import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js'; import {Field} from '../field.js'; import {getFocusManager} from '../focus_manager.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; @@ -38,11 +39,11 @@ export class LineCursor extends Marker { } /** - * Moves the cursor to the next previous connection, next connection or block - * in the pre order traversal. Finds the next node in the pre order traversal. + * Moves the cursor to the next block or workspace comment in the pre-order + * traversal. * - * @returns The next node, or null if the current node is - * not set or there is no next value. + * @returns The next node, or null if the current node is not set or there is + * no next value. */ next(): IFocusableNode | null { const curNode = this.getCurNode(); @@ -53,8 +54,9 @@ export class LineCursor extends Marker { curNode, (candidate: IFocusableNode | null) => { return ( - candidate instanceof BlockSvg && - !candidate.outputConnection?.targetBlock() + (candidate instanceof BlockSvg && + !candidate.outputConnection?.targetBlock()) || + candidate instanceof RenderedWorkspaceComment ); }, true, @@ -87,11 +89,11 @@ export class LineCursor extends Marker { return newNode; } /** - * Moves the cursor to the previous next connection or previous connection in - * the pre order traversal. + * Moves the cursor to the previous block or workspace comment in the + * pre-order traversal. * - * @returns The previous node, or null if the current node - * is not set or there is no previous value. + * @returns The previous node, or null if the current node is not set or there + * is no previous value. */ prev(): IFocusableNode | null { const curNode = this.getCurNode(); @@ -102,8 +104,9 @@ export class LineCursor extends Marker { curNode, (candidate: IFocusableNode | null) => { return ( - candidate instanceof BlockSvg && - !candidate.outputConnection?.targetBlock() + (candidate instanceof BlockSvg && + !candidate.outputConnection?.targetBlock()) || + candidate instanceof RenderedWorkspaceComment ); }, true, diff --git a/core/keyboard_nav/workspace_comment_navigation_policy.ts b/core/keyboard_nav/workspace_comment_navigation_policy.ts new file mode 100644 index 000000000..7fe70cead --- /dev/null +++ b/core/keyboard_nav/workspace_comment_navigation_policy.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import {navigateStacks} from './block_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from an RenderedWorkspaceComment. + */ +export class WorkspaceCommentNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given workspace comment. + * + * @param current The workspace comment to return the first child of. + * @returns The first child button of the given comment. + */ + getFirstChild(current: RenderedWorkspaceComment): IFocusableNode | null { + return current.view.getCommentBarButtons()[0]; + } + + /** + * Returns the parent of the given workspace comment. + * + * @param current The workspace comment to return the parent of. + * @returns The parent workspace of the given comment. + */ + getParent(current: RenderedWorkspaceComment): IFocusableNode | null { + return current.workspace; + } + + /** + * Returns the next peer node of the given workspace comment. + * + * @param current The workspace comment to find the following element of. + * @returns The next workspace comment or block stack, if any. + */ + getNextSibling(current: RenderedWorkspaceComment): IFocusableNode | null { + return navigateStacks(current, 1); + } + + /** + * Returns the previous peer node of the given workspace comment. + * + * @param current The workspace comment to find the preceding element of. + * @returns The previous workspace comment or block stack, if any. + */ + getPreviousSibling(current: RenderedWorkspaceComment): IFocusableNode | null { + return navigateStacks(current, -1); + } + + /** + * Returns whether or not the given workspace comment can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given workspace comment can be focused. + */ + isNavigable(current: RenderedWorkspaceComment): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is an RenderedWorkspaceComment. + */ + isApplicable(current: any): current is RenderedWorkspaceComment { + return current instanceof RenderedWorkspaceComment; + } +} diff --git a/core/navigator.ts b/core/navigator.ts index 77bb64cd8..2f095f6f9 100644 --- a/core/navigator.ts +++ b/core/navigator.ts @@ -7,9 +7,11 @@ import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {INavigationPolicy} from './interfaces/i_navigation_policy.js'; import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js'; +import {CommentBarButtonNavigationPolicy} from './keyboard_nav/comment_bar_button_navigation_policy.js'; import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js'; import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js'; import {IconNavigationPolicy} from './keyboard_nav/icon_navigation_policy.js'; +import {WorkspaceCommentNavigationPolicy} from './keyboard_nav/workspace_comment_navigation_policy.js'; import {WorkspaceNavigationPolicy} from './keyboard_nav/workspace_navigation_policy.js'; type RuleList = INavigationPolicy[]; @@ -29,6 +31,8 @@ export class Navigator { new ConnectionNavigationPolicy(), new WorkspaceNavigationPolicy(), new IconNavigationPolicy(), + new WorkspaceCommentNavigationPolicy(), + new CommentBarButtonNavigationPolicy(), ]; /** diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index 062d0cb4e..f621f93d3 100644 --- a/core/shortcut_items.ts +++ b/core/shortcut_items.ts @@ -11,7 +11,6 @@ import * as clipboard from './clipboard.js'; import {RenderedWorkspaceComment} from './comments.js'; import * as eventUtils from './events/utils.js'; import {getFocusManager} from './focus_manager.js'; -import {Gesture} from './gesture.js'; import {ICopyData, isCopyable as isICopyable} from './interfaces/i_copyable.js'; import {isDeletable as isIDeletable} from './interfaces/i_deletable.js'; import {isDraggable} from './interfaces/i_draggable.js'; @@ -67,7 +66,7 @@ export function registerDelete() { focused != null && isIDeletable(focused) && focused.isDeletable() && - !Gesture.inProgress() && + !workspace.isDragging() && // Don't delete the block if a field editor is open !getFocusManager().ephemeralFocusTaken() ); @@ -322,7 +321,7 @@ export function registerUndo() { preconditionFn(workspace) { return ( !workspace.isReadOnly() && - !Gesture.inProgress() && + !workspace.isDragging() && !getFocusManager().ephemeralFocusTaken() ); }, @@ -360,7 +359,7 @@ export function registerRedo() { name: names.REDO, preconditionFn(workspace) { return ( - !Gesture.inProgress() && + !workspace.isDragging() && !workspace.isReadOnly() && !getFocusManager().ephemeralFocusTaken() ); diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 57e849ce2..4979fdfa4 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -43,6 +43,7 @@ import type {KeyboardShortcut} from '../shortcut_registry.js'; import * as Touch from '../touch.js'; import * as aria from '../utils/aria.js'; import * as dom from '../utils/dom.js'; +import * as idGenerator from '../utils/idgenerator.js'; import {Rect} from '../utils/rect.js'; import * as toolbox from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; @@ -185,6 +186,7 @@ export class Toolbox const svg = workspace.getParentSvg(); const container = this.createContainer_(); + container.id = idGenerator.getNextUniqueId(); this.contentsDiv_ = this.createContentsContainer_(); aria.setRole(this.contentsDiv_, aria.Role.TREE); @@ -1170,6 +1172,7 @@ Css.register(` /* Category tree in Toolbox. */ .blocklyToolbox { + box-sizing: border-box; user-select: none; -ms-user-select: none; -webkit-user-select: none; diff --git a/core/utils/focusable_tree_traverser.ts b/core/utils/focusable_tree_traverser.ts index 916437b6a..aa4585b82 100644 --- a/core/utils/focusable_tree_traverser.ts +++ b/core/utils/focusable_tree_traverser.ts @@ -79,8 +79,8 @@ export class FocusableTreeTraverser { * traversed but its nodes will never be returned here per the contract of * IFocusableTree.lookUpFocusableNode. * - * The provided element must have a non-null ID that conforms to the contract - * mentioned in IFocusableNode. + * The provided element must have a non-null, non-empty ID that conforms to + * the contract mentioned in IFocusableNode. * * @param element The HTML or SVG element being sought. * @param tree The tree under which the provided element may be a descendant. @@ -90,6 +90,10 @@ export class FocusableTreeTraverser { element: HTMLElement | SVGElement, tree: IFocusableTree, ): IFocusableNode | null { + // Note that the null check is due to Element.setAttribute() converting null + // to a string. + if (!element.id || element.id === 'null') return null; + // First, match against subtrees. const subTreeMatches = tree.getNestedTrees().map((tree) => { return FocusableTreeTraverser.findFocusableNodeFor(element, tree); diff --git a/core/utils/rect.ts b/core/utils/rect.ts index c7da2a686..5a6822633 100644 --- a/core/utils/rect.ts +++ b/core/utils/rect.ts @@ -32,6 +32,16 @@ export class Rect { public right: number, ) {} + /** + * Converts a DOM or SVG Rect to a Blockly Rect. + * + * @param rect The rectangle to convert. + * @returns A representation of the same rectangle as a Blockly Rect. + */ + static from(rect: DOMRect | SVGRect): Rect { + return new Rect(rect.y, rect.y + rect.height, rect.x, rect.x + rect.width); + } + /** * Creates a new copy of this rectangle. * @@ -51,6 +61,11 @@ export class Rect { return this.right - this.left; } + /** Returns the top left coordinate of this rectangle. */ + getOrigin(): Coordinate { + return new Coordinate(this.left, this.top); + } + /** * Tests whether this rectangle contains a x/y coordinate. * diff --git a/core/workspace.ts b/core/workspace.ts index f7b866447..5f2051939 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -21,6 +21,7 @@ import * as common from './common.js'; import type {ConnectionDB} from './connection_db.js'; import type {Abstract} from './events/events_abstract.js'; import * as eventUtils from './events/utils.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; import {IProcedureMap} from './interfaces/i_procedure_map.js'; import type {IVariableMap} from './interfaces/i_variable_map.js'; @@ -35,6 +36,7 @@ import * as arrayUtils from './utils/array.js'; import * as deprecation from './utils/deprecation.js'; import * as idGenerator from './utils/idgenerator.js'; import * as math from './utils/math.js'; +import {Rect} from './utils/rect.js'; import type * as toolbox from './utils/toolbox.js'; import {deleteVariable, getVariableUsesById} from './variables.js'; @@ -181,10 +183,31 @@ export class Workspace { a: Block | WorkspaceComment, b: Block | WorkspaceComment, ): number { + const wrap = (element: Block | WorkspaceComment) => { + return { + getBoundingRectangle: () => { + const xy = element.getRelativeToSurfaceXY(); + return new Rect(xy.y, xy.y, xy.x, xy.x); + }, + moveBy: () => {}, + }; + }; + return this.sortByOrigin(wrap(a), wrap(b)); + } + + /** + * Sorts bounded elements on the workspace by their relative position, top to + * bottom (with slight LTR or RTL bias). + * + * @param a The first element to sort. + * @param b The second elment to sort. + * @returns -1, 0 or 1 depending on the sort order. + */ + protected sortByOrigin(a: IBoundedElement, b: IBoundedElement): number { const offset = Math.sin(math.toRadians(Workspace.SCAN_ANGLE)) * (this.RTL ? -1 : 1); - const aXY = a.getRelativeToSurfaceXY(); - const bXY = b.getRelativeToSurfaceXY(); + const aXY = a.getBoundingRectangle().getOrigin(); + const bXY = b.getBoundingRectangle().getOrigin(); return aXY.y + offset * aXY.x - (bXY.y + offset * bXY.x); } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 3033eacd7..00eef5653 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -22,7 +22,9 @@ import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; +import {COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/collapse_comment_bar_button.js'; import {COMMENT_EDITOR_FOCUS_IDENTIFIER} from './comments/comment_editor.js'; +import {COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/delete_comment_bar_button.js'; import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; import {WorkspaceComment} from './comments/workspace_comment.js'; import * as common from './common.js'; @@ -2266,8 +2268,8 @@ export class WorkspaceSvg * * @param comment comment to add. */ - override addTopComment(comment: WorkspaceComment) { - this.addTopBoundedElement(comment as RenderedWorkspaceComment); + override addTopComment(comment: RenderedWorkspaceComment) { + this.addTopBoundedElement(comment); super.addTopComment(comment); } @@ -2276,11 +2278,31 @@ export class WorkspaceSvg * * @param comment comment to remove. */ - override removeTopComment(comment: WorkspaceComment) { - this.removeTopBoundedElement(comment as RenderedWorkspaceComment); + override removeTopComment(comment: RenderedWorkspaceComment) { + this.removeTopBoundedElement(comment); super.removeTopComment(comment); } + /** + * Returns a list of comments on this workspace. + * + * @param ordered If true, sorts the comments based on their position. + * @returns A list of workspace comments. + */ + override getTopComments(ordered = false): RenderedWorkspaceComment[] { + return super.getTopComments(ordered) as RenderedWorkspaceComment[]; + } + + /** + * Returns the workspace comment with the given ID, if any. + * + * @param id The ID of the comment to retrieve. + * @returns The workspace comment with the given ID, or null. + */ + override getCommentById(id: string): RenderedWorkspaceComment | null { + return super.getCommentById(id) as RenderedWorkspaceComment | null; + } + override getRootWorkspace(): WorkspaceSvg | null { return super.getRootWorkspace() as WorkspaceSvg | null; } @@ -2308,8 +2330,15 @@ export class WorkspaceSvg * * @returns The top-level bounded elements. */ - getTopBoundedElements(): IBoundedElement[] { - return new Array().concat(this.topBoundedElements); + getTopBoundedElements(ordered = false): IBoundedElement[] { + const elements = new Array().concat( + this.topBoundedElements, + ); + if (ordered) { + elements.sort(this.sortByOrigin.bind(this)); + } + + return elements; } /** @@ -2794,19 +2823,32 @@ export class WorkspaceSvg return null; } - // Search for a specific workspace comment editor - // (only if id seems like it is one). - const commentEditorIndicator = id.indexOf(COMMENT_EDITOR_FOCUS_IDENTIFIER); - if (commentEditorIndicator !== -1) { - const commentId = id.substring(0, commentEditorIndicator); + // Search for a specific workspace comment or comment icon if the ID + // indicates the presence of one. + const commentIdSeparatorIndex = Math.max( + id.indexOf(COMMENT_EDITOR_FOCUS_IDENTIFIER), + id.indexOf(COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER), + id.indexOf(COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER), + ); + if (commentIdSeparatorIndex !== -1) { + const commentId = id.substring(0, commentIdSeparatorIndex); const comment = this.searchForWorkspaceComment(commentId); if (comment) { - return comment.getEditorFocusableNode(); + if (id.indexOf(COMMENT_EDITOR_FOCUS_IDENTIFIER) > -1) { + return comment.getEditorFocusableNode(); + } else { + return ( + comment.view + .getCommentBarButtons() + .find((button) => button.getFocusableElement().id.includes(id)) ?? + null + ); + } } } // Search for a specific block. - // Don't use `getBlockById` because the block ID is not guaranteeed + // Don't use `getBlockById` because the block ID is not guaranteed // to be globally unique, but the ID on the focusable element is. const block = this.getAllBlocks(false).find( (block) => block.getFocusableElement().id === id, diff --git a/eslint.config.mjs b/eslint.config.mjs index 68f25133f..f018e525d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -184,7 +184,7 @@ export default [ files: [ 'eslint.config.mjs', '.prettierrc.js', - 'gulpfile.js', + 'gulpfile.mjs', 'scripts/helpers.js', 'tests/mocha/.mocharc.js', 'tests/migration/validate-renamings.mjs', diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index d2ad650c6..000000000 --- a/gulpfile.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Gulp script to build Blockly for Node & NPM. - * Run this script by calling "npm install" in this directory. - */ -/* eslint-env node */ - -const gulp = require('gulp'); - -const buildTasks = require('./scripts/gulpfiles/build_tasks'); -const packageTasks = require('./scripts/gulpfiles/package_tasks'); -const gitTasks = require('./scripts/gulpfiles/git_tasks'); -const appengineTasks = require('./scripts/gulpfiles/appengine_tasks'); -const releaseTasks = require('./scripts/gulpfiles/release_tasks'); -const docsTasks = require('./scripts/gulpfiles/docs_tasks'); -const testTasks = require('./scripts/gulpfiles/test_tasks'); - -module.exports = { - // Default target if gulp invoked without specifying. - default: buildTasks.build, - - // Main sequence targets. They already invoke prerequisites. - langfiles: buildTasks.langfiles, // Build build/msg/*.js from msg/json/*. - tsc: buildTasks.tsc, - deps: buildTasks.deps, - minify: buildTasks.minify, - build: buildTasks.build, - package: packageTasks.package, - publish: releaseTasks.publish, - publishBeta: releaseTasks.publishBeta, - prepareDemos: appengineTasks.prepareDemos, - deployDemos: appengineTasks.deployDemos, - deployDemosBeta: appengineTasks.deployDemosBeta, - gitUpdateGithubPages: gitTasks.updateGithubPages, - - // Manually-invokable targets, with prerequisites where required. - messages: buildTasks.messages, // Generate msg/json/en.json et al. - clean: gulp.parallel(buildTasks.cleanBuildDir, packageTasks.cleanReleaseDir), - test: testTasks.test, - testGenerators: testTasks.generators, - buildAdvancedCompilationTest: buildTasks.buildAdvancedCompilationTest, - gitCreateRC: gitTasks.createRC, - docs: docsTasks.docs, - - // Legacy targets, to be deleted. - recompile: releaseTasks.recompile, - gitSyncDevelop: gitTasks.syncDevelop, - gitSyncMaster: gitTasks.syncMaster, -}; diff --git a/gulpfile.mjs b/gulpfile.mjs new file mode 100644 index 000000000..fd3de3bde --- /dev/null +++ b/gulpfile.mjs @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Gulp script to build Blockly for Node & NPM. + * Run this script by calling "npm install" in this directory. + */ +/* eslint-env node */ + +// Needed to prevent prettier from munging exports order, due to +// https://github.com/simonhaenisch/prettier-plugin-organize-imports/issues/146 +// - but has the unfortunate side effect of suppressing ordering of +// imports too: +// +// organize-imports-ignore + +import {parallel} from 'gulp'; +import { + deployDemos, + deployDemosBeta, + prepareDemos, +} from './scripts/gulpfiles/appengine_tasks.mjs'; +import { + build, + buildAdvancedCompilationTest, + cleanBuildDir, + langfiles, + messages, + minify, + tsc, +} from './scripts/gulpfiles/build_tasks.mjs'; +import {docs} from './scripts/gulpfiles/docs_tasks.mjs'; +import { + createRC, + syncDevelop, + syncMaster, + updateGithubPages, +} from './scripts/gulpfiles/git_tasks.mjs'; +import {cleanReleaseDir, pack} from './scripts/gulpfiles/package_tasks.mjs'; +import { + publish, + publishBeta, + recompile, +} from './scripts/gulpfiles/release_tasks.mjs'; +import {generators, test} from './scripts/gulpfiles/test_tasks.mjs'; + +const clean = parallel(cleanBuildDir, cleanReleaseDir); + +// Default target if gulp invoked without specifying. +export default build; + +// Main sequence targets. They already invoke prerequisites. Listed +// in typical order of invocation, and strictly listing prerequisites +// before dependants. +// +// prettier-ignore +export { + langfiles, + tsc, + minify, + build, + pack, // Formerly package. + publishBeta, + publish, + prepareDemos, + deployDemosBeta, + deployDemos, + updateGithubPages as gitUpdateGithubPages, +} + +// Manually-invokable targets that also invoke prerequisites where +// required. +// +// prettier-ignore +export { + messages, // Generate msg/json/en.json et al. + clean, + test, + generators as testGenerators, + buildAdvancedCompilationTest, + createRC as gitCreateRC, + docs, +} + +// Legacy targets, to be deleted. +// +// prettier-ignore +export { + recompile, + syncDevelop as gitSyncDevelop, + syncMaster as gitSyncMaster, +} diff --git a/package-lock.json b/package-lock.json index 8f8de5349..d1977538e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "jsdom": "26.1.0" }, "devDependencies": { - "@blockly/block-test": "^6.0.4", + "@blockly/block-test": "^7.0.1", "@blockly/dev-tools": "^9.0.0", "@blockly/theme-modern": "^6.0.3", "@hyperjump/browser": "^1.1.4", @@ -26,7 +26,7 @@ "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", - "eslint-plugin-jsdoc": "^50.5.0", + "eslint-plugin-jsdoc": "^51.3.1", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", @@ -89,15 +89,15 @@ "license": "ISC" }, "node_modules/@blockly/block-test": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-6.0.11.tgz", - "integrity": "sha512-aIgcxkof1gLJtJXKSvmnug9iSXbv5Qilnov4Sa/QNURiWJRxvMNqWiTZJVu/reuCQK4Qm4jadg9R9l+eu7ujvw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.1.tgz", + "integrity": "sha512-w91ZZbpJDKGQJVO7gKqQaM17ffcsW1ktrnSTz/OpDw5R4H+1q05NgWO5gYzGPzLfFdvPcrkc0v00KhD4UG7BRA==", "dev": true, "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@blockly/dev-tools": { @@ -126,19 +126,6 @@ "blockly": "^12.0.0" } }, - "node_modules/@blockly/dev-tools/node_modules/@blockly/block-test": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.1.tgz", - "integrity": "sha512-w91ZZbpJDKGQJVO7gKqQaM17ffcsW1ktrnSTz/OpDw5R4H+1q05NgWO5gYzGPzLfFdvPcrkc0v00KhD4UG7BRA==", - "dev": true, - "license": "Apache 2.0", - "engines": { - "node": ">=8.17.0" - }, - "peerDependencies": { - "blockly": "^12.0.0" - } - }, "node_modules/@blockly/dev-tools/node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -383,20 +370,32 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.50.2", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.50.2.tgz", - "integrity": "sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.52.0.tgz", + "integrity": "sha512-BXuN7BII+8AyNtn57euU2Yxo9yA/KUDNzrpXyi3pfqKmBhhysR6ZWOebFh3vyPoqA3/j1SOvGgucElMGwlXing==", "dev": true, - "license": "MIT", "dependencies": { - "@types/estree": "^1.0.6", - "@typescript-eslint/types": "^8.11.0", + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.34.1", "comment-parser": "1.4.1", "esquery": "^1.6.0", "jsdoc-type-pratt-parser": "~4.1.0" }, "engines": { - "node": ">=18" + "node": ">=20.11.0" + } + }, + "node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@eslint-community/eslint-utils": { @@ -428,11 +427,10 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -443,21 +441,19 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -503,13 +499,15 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", + "integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -517,25 +515,35 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gulp-sourcemaps/identity-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", @@ -792,6 +800,27 @@ "url": "https://github.com/sponsors/jdesrosiers" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1144,28 +1173,6 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", - "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1479,9 +1486,9 @@ "dev": true }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, "node_modules/@types/expect": { @@ -2029,24 +2036,10 @@ "node": ">=6.5" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2255,30 +2248,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/archiver-utils/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/archiver-utils/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2401,30 +2370,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/archiver/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/archiver/node_modules/buffer-crc32": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", @@ -2807,51 +2752,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/bl/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2886,6 +2786,30 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -3181,7 +3105,6 @@ "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 12.0.0" } @@ -3202,30 +3125,6 @@ "node": ">= 14" } }, - "node_modules/compress-commons/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/compress-commons/node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -3365,40 +3264,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -3417,26 +3282,6 @@ "safe-buffer": "~5.1.1" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/copy-props": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", @@ -3456,20 +3301,6 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/corser": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", @@ -3504,30 +3335,6 @@ "node": ">= 14" } }, - "node_modules/crc32-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/crc32-stream/node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -3803,16 +3610,6 @@ "node": ">= 14" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4008,29 +3805,12 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -4162,13 +3942,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4215,24 +3988,22 @@ } }, "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz", + "integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.30.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -4240,9 +4011,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4256,8 +4027,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -4290,38 +4060,39 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", - "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, - "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } }, "node_modules/eslint-plugin-jsdoc": { - "version": "50.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.7.1.tgz", - "integrity": "sha512-XBnVA5g2kUVokTNUiE1McEPse5n9/mNUmuJcx52psT6zBs2eVcXSmQBvjfa7NZdfLVSy3u1pEDDUxoxpwy89WA==", + "version": "51.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.3.1.tgz", + "integrity": "sha512-9v/e6XyrLf1HIs/uPCgm3GcUpH4BeuGVZJk7oauKKyS7su7d5Q6zx4Fq6TiYh+w7+b4Svy7ZWVCcNZJNx3y52w==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.50.2", + "@es-joy/jsdoccomment": "~0.52.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", "debug": "^4.4.1", "escape-string-regexp": "^4.0.0", - "espree": "^10.3.0", + "espree": "^10.4.0", "esquery": "^1.6.0", "parse-imports-exports": "^0.2.4", "semver": "^7.7.2", "spdx-expression-parse": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=20.11.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" @@ -4382,11 +4153,10 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -4411,9 +4181,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4435,14 +4205,14 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4452,9 +4222,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4493,7 +4263,6 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -4519,16 +4288,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -4563,29 +4322,6 @@ "node": ">=0.8.x" } }, - "node_modules/eventsource": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", - "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -4598,65 +4334,6 @@ "node": ">=0.10.0" } }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, "node_modules/ext": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", @@ -4870,24 +4547,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5023,12 +4682,12 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -5051,26 +4710,6 @@ "node": ">=12.20.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -5293,15 +4932,14 @@ } }, "node_modules/glob": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", - "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, - "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -5329,9 +4967,9 @@ } }, "node_modules/glob-stream": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", - "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.3.tgz", + "integrity": "sha512-fqZVj22LtFJkHODT+M4N1RJQ3TjnnQhfE9GwZI8qXscYarnhpip70poMldRnP8ipQ/w0B621kOhfc53/J9bd/A==", "dev": true, "dependencies": { "@gulpjs/to-absolute-glob": "^4.0.0", @@ -5372,24 +5010,13 @@ "node": ">= 10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -5571,15 +5198,15 @@ "license": "MIT" }, "node_modules/gulp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", - "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.1.tgz", + "integrity": "sha512-PErok3DZSA5WGMd6XXV3IRNO0mlB+wW3OzhFJLEec1jSERg2j1bxJ6e5Fh6N6fn3FH2T9AP4UYNb/pYlADB9sA==", "dev": true, "dependencies": { "glob-watcher": "^6.0.0", - "gulp-cli": "^3.0.0", + "gulp-cli": "^3.1.0", "undertaker": "^2.0.0", - "vinyl-fs": "^4.0.0" + "vinyl-fs": "^4.0.2" }, "bin": { "gulp": "bin/gulp.js" @@ -5589,9 +5216,9 @@ } }, "node_modules/gulp-cli": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", - "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.1.0.tgz", + "integrity": "sha512-zZzwlmEsTfXcxRKiCHsdyjZZnFvXWM4v1NqBJSYbuApkvVKivjcmOS2qruAJ+PkEHLFavcDKH40DPc1+t12a9Q==", "dev": true, "dependencies": { "@gulpjs/messages": "^1.1.0", @@ -5599,7 +5226,7 @@ "copy-props": "^4.0.0", "gulplog": "^2.2.0", "interpret": "^3.1.1", - "liftoff": "^5.0.0", + "liftoff": "^5.0.1", "mute-stdout": "^2.0.0", "replace-homedir": "^2.0.0", "semver-greatest-satisfied-range": "^2.0.0", @@ -6085,23 +5712,6 @@ "entities": "^4.5.0" } }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -6311,16 +5921,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -6581,11 +6181,10 @@ } }, "node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -6626,7 +6225,6 @@ "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12.0.0" } @@ -6890,9 +6488,9 @@ } }, "node_modules/liftoff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", - "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.1.tgz", + "integrity": "sha512-wwLXMbuxSF8gMvubFcFRp56lkFV69twvbU5vDPbaw+Q+/rF8j0HKjGbIdlSi+LuJm9jf7k9PB+nTxnsLMPcv2Q==", "dev": true, "dependencies": { "extend": "^3.0.2", @@ -7119,16 +6717,6 @@ "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -7151,19 +6739,6 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7200,29 +6775,6 @@ "node": ">=4" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7264,11 +6816,10 @@ } }, "node_modules/mocha": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.0.tgz", - "integrity": "sha512-bXfLy/mI8n4QICg+pWj1G8VduX5vC0SHRwFpiR5/Fxc8S2G906pSfkyMmHVsdJNQJQNh3LE67koad9GzEvkV6g==", + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", + "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", "dev": true, - "license": "MIT", "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", @@ -7464,16 +7015,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -7631,19 +7172,6 @@ "node": ">=0.10.0" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7880,16 +7408,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/patch-package": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", @@ -8106,16 +7624,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/plugin-error": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", @@ -8211,11 +7719,10 @@ } }, "node_modules/prettier": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.0.tgz", - "integrity": "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, - "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -8279,20 +7786,6 @@ "node": ">=0.4.0" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-agent": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", @@ -8407,32 +7900,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -8778,40 +8245,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -8922,29 +8355,6 @@ "node": ">= 10.13.0" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/serialize-error": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", @@ -8969,35 +8379,12 @@ "randombytes": "^2.1.0" } }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9257,16 +8644,6 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/stream-composer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", @@ -9661,16 +9038,6 @@ "node": ">=10.13.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -9762,21 +9129,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -9899,16 +9251,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -9973,16 +9315,6 @@ "node": ">= 10.13.0" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vinyl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", @@ -10014,13 +9346,12 @@ } }, "node_modules/vinyl-contents/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", "dev": true, "dependencies": { "clone": "^2.1.2", - "clone-stats": "^1.0.0", "remove-trailing-separator": "^1.1.0", "replace-ext": "^2.0.0", "teex": "^1.0.1" @@ -10030,13 +9361,13 @@ } }, "node_modules/vinyl-fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", - "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.2.tgz", + "integrity": "sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA==", "dev": true, "dependencies": { "fs-mkdirp-stream": "^2.0.1", - "glob-stream": "^8.0.0", + "glob-stream": "^8.0.3", "graceful-fs": "^4.2.11", "iconv-lite": "^0.6.3", "is-valid-glob": "^1.0.0", @@ -10047,7 +9378,7 @@ "streamx": "^2.14.0", "to-through": "^3.0.0", "value-or-function": "^4.0.0", - "vinyl": "^3.0.0", + "vinyl": "^3.0.1", "vinyl-sourcemap": "^2.0.0" }, "engines": { @@ -10055,13 +9386,12 @@ } }, "node_modules/vinyl-fs/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", "dev": true, "dependencies": { "clone": "^2.1.2", - "clone-stats": "^1.0.0", "remove-trailing-separator": "^1.1.0", "replace-ext": "^2.0.0", "teex": "^1.0.1" @@ -10094,13 +9424,12 @@ "dev": true }, "node_modules/vinyl-sourcemap/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", "dev": true, "dependencies": { "clone": "^2.1.2", - "clone-stats": "^1.0.0", "remove-trailing-separator": "^1.1.0", "replace-ext": "^2.0.0", "teex": "^1.0.1" @@ -10542,30 +9871,6 @@ "node": ">= 14" } }, - "node_modules/zip-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/zip-stream/node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -10610,26 +9915,6 @@ "dependencies": { "safe-buffer": "~5.2.0" } - }, - "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/package.json b/package.json index eab39b16c..030eed6fd 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "lint:fix": "eslint . --fix", "langfiles": "gulp langfiles", "minify": "gulp minify", - "package": "gulp package", + "package": "gulp pack", "postinstall": "patch-package", "prepareDemos": "gulp prepareDemos", "publish": "npm ci && gulp publish", @@ -100,7 +100,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@blockly/block-test": "^6.0.4", + "@blockly/block-test": "^7.0.1", "@blockly/dev-tools": "^9.0.0", "@blockly/theme-modern": "^6.0.3", "@hyperjump/browser": "^1.1.4", @@ -113,7 +113,7 @@ "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", - "eslint-plugin-jsdoc": "^50.5.0", + "eslint-plugin-jsdoc": "^51.3.1", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", diff --git a/scripts/gulpfiles/appengine_tasks.js b/scripts/gulpfiles/appengine_tasks.mjs similarity index 86% rename from scripts/gulpfiles/appengine_tasks.js rename to scripts/gulpfiles/appengine_tasks.mjs index ddbd2f45f..754534383 100644 --- a/scripts/gulpfiles/appengine_tasks.js +++ b/scripts/gulpfiles/appengine_tasks.mjs @@ -8,16 +8,16 @@ * @fileoverview Gulp script to deploy Blockly demos on appengine. */ -const gulp = require('gulp'); +import * as gulp from 'gulp'; -const fs = require('fs'); -const path = require('path'); -const execSync = require('child_process').execSync; -const buildTasks = require('./build_tasks.js'); -const packageTasks = require('./package_tasks.js'); -const {rimraf} = require('rimraf'); +import * as fs from 'fs'; +import * as path from 'path'; +import {execSync} from 'child_process'; +import * as buildTasks from './build_tasks.mjs'; +import {getPackageJson} from './helper_tasks.mjs'; +import * as packageTasks from './package_tasks.mjs'; +import {rimraf} from 'rimraf'; -const packageJson = require('../../package.json'); const demoTmpDir = '../_deploy'; const demoStaticTmpDir = '../_deploy/static'; @@ -123,7 +123,7 @@ function deployToAndClean(demoVersion) { */ function getDemosVersion() { // Replace all '.' with '-' e.g. 9-3-3-beta-2 - return packageJson.version.replace(/\./g, '-'); + return getPackageJson().version.replace(/\./g, '-'); } /** @@ -162,7 +162,7 @@ function deployBetaAndClean(done) { * * Prerequisites (invoked): clean, build */ -const prepareDemos = gulp.series( +export const prepareDemos = gulp.series( prepareDeployDir, gulp.parallel( gulp.series( @@ -180,16 +180,9 @@ const prepareDemos = gulp.series( /** * Deploys demos. */ -const deployDemos = gulp.series(prepareDemos, deployAndClean); +export const deployDemos = gulp.series(prepareDemos, deployAndClean); /** * Deploys beta version of demos (version appended with -beta). */ -const deployDemosBeta = gulp.series(prepareDemos, deployBetaAndClean); - -module.exports = { - // Main sequence targets. Each should invoke any immediate prerequisite(s). - deployDemos: deployDemos, - deployDemosBeta: deployDemosBeta, - prepareDemos: prepareDemos -}; +export const deployDemosBeta = gulp.series(prepareDemos, deployBetaAndClean); diff --git a/scripts/gulpfiles/build_tasks.js b/scripts/gulpfiles/build_tasks.mjs similarity index 92% rename from scripts/gulpfiles/build_tasks.js rename to scripts/gulpfiles/build_tasks.mjs index a00c1b17d..669e73258 100644 --- a/scripts/gulpfiles/build_tasks.js +++ b/scripts/gulpfiles/build_tasks.mjs @@ -8,25 +8,32 @@ * @fileoverview Gulp script to build Blockly for Node & NPM. */ -const gulp = require('gulp'); -gulp.replace = require('gulp-replace'); -gulp.rename = require('gulp-rename'); -gulp.sourcemaps = require('gulp-sourcemaps'); +import * as gulp from 'gulp'; +import replace from 'gulp-replace'; +import rename from 'gulp-rename'; +import sourcemaps from 'gulp-sourcemaps'; -const path = require('path'); -const fs = require('fs'); -const fsPromises = require('fs/promises'); -const {exec, execSync} = require('child_process'); +import * as path from 'path'; +import * as fs from 'fs'; +import * as fsPromises from 'fs/promises'; +import {exec, execSync} from 'child_process'; -const {globSync} = require('glob'); -const closureCompiler = require('google-closure-compiler').gulp(); -const argv = require('yargs').argv; -const {rimraf} = require('rimraf'); +import {globSync} from 'glob'; +// For v20250609.0.0 and later: +// import {gulp as closureCompiler} from 'google-closure-compiler'; +import ClosureCompiler from 'google-closure-compiler'; +import yargs from 'yargs'; +import {hideBin} from 'yargs/helpers'; +import {rimraf} from 'rimraf'; -const {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TSC_OUTPUT_DIR, TYPINGS_BUILD_DIR} = require('./config'); -const {getPackageJson} = require('./helper_tasks'); +import {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TSC_OUTPUT_DIR, TYPINGS_BUILD_DIR} from './config.mjs'; +import {getPackageJson} from './helper_tasks.mjs'; -const {posixPath, quote} = require('../helpers'); +import {posixPath, quote} from '../helpers.js'; + +const closureCompiler = ClosureCompiler.gulp(); + +const argv = yargs(hideBin(process.argv)).parse(); //////////////////////////////////////////////////////////// // Build // @@ -182,7 +189,7 @@ function stripApacheLicense() { // Closure Compiler preserves dozens of Apache licences in the Blockly code. // Remove these if they belong to Google or MIT. // MIT's permission to do this is logged in Blockly issue #2412. - return gulp.replace(new RegExp(licenseRegex, 'g'), '\n\n\n\n'); + return replace(new RegExp(licenseRegex, 'g'), '\n\n\n\n'); // Replace with the same number of lines so that source-maps are not affected. } @@ -306,7 +313,7 @@ const JSCOMP_OFF = [ * Builds Blockly as a JS program, by running tsc on all the files in * the core directory. */ -function buildJavaScript(done) { +export function tsc(done) { execSync( `tsc -outDir "${TSC_OUTPUT_DIR}" -declarationDir "${TYPINGS_BUILD_DIR}"`, {stdio: 'inherit'}); @@ -318,7 +325,7 @@ function buildJavaScript(done) { * This task regenerates msg/json/en.js and msg/json/qqq.js from * msg/messages.js. */ -function generateMessages(done) { +export function messages(done) { // Run js_to_json.py const jsToJsonCmd = `${PYTHON} scripts/i18n/js_to_json.py \ --input_file ${path.join('msg', 'messages.js')} \ @@ -573,10 +580,10 @@ function buildCompiled() { // Fire up compilation pipline. return gulp.src(chunkOptions.js, {base: './'}) .pipe(stripApacheLicense()) - .pipe(gulp.sourcemaps.init()) + .pipe(sourcemaps.init()) .pipe(compile(options)) - .pipe(gulp.rename({suffix: COMPILED_SUFFIX})) - .pipe(gulp.sourcemaps.write('.')) + .pipe(rename({suffix: COMPILED_SUFFIX})) + .pipe(sourcemaps.write('.')) .pipe(gulp.dest(RELEASE_DIR)); } @@ -668,7 +675,7 @@ async function buildLangfileShims() { // (We have to do it this way because messages.js is a script and // not a CJS module with exports.) globalThis.Blockly = {Msg: {}}; - require('../../msg/messages.js'); + await import('../../msg/messages.js'); const exportedNames = Object.keys(globalThis.Blockly.Msg); delete globalThis.Blockly; @@ -689,12 +696,14 @@ ${exportedNames.map((name) => ` ${name},`).join('\n')} } /** - * This task builds Blockly core, blocks and generators together and uses - * Closure Compiler's ADVANCED_COMPILATION mode. + * This task uses Closure Compiler's ADVANCED_COMPILATION mode to + * compile together Blockly core, blocks and generators with a simple + * test app; the purpose is to verify that Blockly is compatible with + * the ADVANCED_COMPILATION mode. * * Prerequisite: buildJavaScript. */ -function buildAdvancedCompilationTest() { +function compileAdvancedCompilationTest() { // If main_compressed.js exists (from a previous run) delete it so that // a later browser-based test won't check it should the compile fail. try { @@ -718,9 +727,9 @@ function buildAdvancedCompilationTest() { }; return gulp.src(srcs, {base: './'}) .pipe(stripApacheLicense()) - .pipe(gulp.sourcemaps.init()) + .pipe(sourcemaps.init()) .pipe(compile(options)) - .pipe(gulp.sourcemaps.write( + .pipe(sourcemaps.write( '.', {includeContent: false, sourceRoot: '../../'})) .pipe(gulp.dest('./tests/compile/')); } @@ -728,7 +737,7 @@ function buildAdvancedCompilationTest() { /** * This task cleans the build directory (by deleting it). */ -function cleanBuildDir() { +export function cleanBuildDir() { // Sanity check. if (BUILD_DIR === '.' || BUILD_DIR === '/') { return Promise.reject(`Refusing to rm -rf ${BUILD_DIR}`); @@ -737,16 +746,13 @@ function cleanBuildDir() { } // Main sequence targets. Each should invoke any immediate prerequisite(s). -exports.cleanBuildDir = cleanBuildDir; -exports.langfiles = gulp.parallel(buildLangfiles, buildLangfileShims); -exports.tsc = buildJavaScript; -exports.minify = gulp.series(exports.tsc, buildCompiled, buildShims); -exports.build = gulp.parallel(exports.minify, exports.langfiles); +// function cleanBuildDir, above +export const langfiles = gulp.parallel(buildLangfiles, buildLangfileShims); +export const minify = gulp.series(tsc, buildCompiled, buildShims); +// function tsc, above +export const build = gulp.parallel(minify, langfiles); // Manually-invokable targets, with prerequisites where required. -exports.messages = generateMessages; // Generate msg/json/en.json et al. -exports.buildAdvancedCompilationTest = - gulp.series(exports.tsc, buildAdvancedCompilationTest); - -// Targets intended only for invocation by scripts; may omit prerequisites. -exports.onlyBuildAdvancedCompilationTest = buildAdvancedCompilationTest; +// function messages, above +export const buildAdvancedCompilationTest = + gulp.series(tsc, compileAdvancedCompilationTest); diff --git a/scripts/gulpfiles/config.js b/scripts/gulpfiles/config.mjs similarity index 70% rename from scripts/gulpfiles/config.js rename to scripts/gulpfiles/config.mjs index 90cd57109..52e4cd06f 100644 --- a/scripts/gulpfiles/config.js +++ b/scripts/gulpfiles/config.mjs @@ -8,7 +8,7 @@ * @fileoverview Common configuration for Gulp scripts. */ -const path = require('path'); +import * as path from 'path'; // Paths are all relative to the repository root. Do not include // trailing slash. @@ -21,21 +21,21 @@ const path = require('path'); // - tests/scripts/update_metadata.sh // Directory to write compiled output to. -exports.BUILD_DIR = 'build'; +export const BUILD_DIR = 'build'; // Directory to write typings output to. -exports.TYPINGS_BUILD_DIR = path.join(exports.BUILD_DIR, 'declarations'); +export const TYPINGS_BUILD_DIR = path.join(BUILD_DIR, 'declarations'); // Directory to write langfile output to. -exports.LANG_BUILD_DIR = path.join(exports.BUILD_DIR, 'msg'); +export const LANG_BUILD_DIR = path.join(BUILD_DIR, 'msg'); // Directory where typescript compiler output can be found. // Matches the value in tsconfig.json: outDir -exports.TSC_OUTPUT_DIR = path.join(exports.BUILD_DIR, 'src'); +export const TSC_OUTPUT_DIR = path.join(BUILD_DIR, 'src'); // Directory for files generated by compiling test code. -exports.TEST_TSC_OUTPUT_DIR = path.join(exports.BUILD_DIR, 'tests'); +export const TEST_TSC_OUTPUT_DIR = path.join(BUILD_DIR, 'tests'); // Directory in which to assemble (and from which to publish) the // blockly npm package. -exports.RELEASE_DIR = 'dist'; +export const RELEASE_DIR = 'dist'; diff --git a/scripts/gulpfiles/docs_tasks.js b/scripts/gulpfiles/docs_tasks.mjs similarity index 94% rename from scripts/gulpfiles/docs_tasks.js rename to scripts/gulpfiles/docs_tasks.mjs index 8820a586f..63fdbe665 100644 --- a/scripts/gulpfiles/docs_tasks.js +++ b/scripts/gulpfiles/docs_tasks.mjs @@ -1,9 +1,9 @@ -const {execSync} = require('child_process'); -const {Extractor} = require('markdown-tables-to-json'); -const fs = require('fs'); -const gulp = require('gulp'); -const header = require('gulp-header'); -const replace = require('gulp-replace'); +import {execSync} from 'child_process'; +import {Extractor} from 'markdown-tables-to-json'; +import * as fs from 'fs'; +import * as gulp from 'gulp'; +import * as header from 'gulp-header'; +import * as replace from 'gulp-replace'; const DOCS_DIR = 'docs'; @@ -140,8 +140,7 @@ const createToc = function(done) { done(); } -const docs = gulp.series( +export const docs = gulp.series( generateApiJson, removeRenames, generateDocs, gulp.parallel(prependBook, createToc)); -module.exports = {docs}; diff --git a/scripts/gulpfiles/git_tasks.js b/scripts/gulpfiles/git_tasks.mjs similarity index 86% rename from scripts/gulpfiles/git_tasks.js rename to scripts/gulpfiles/git_tasks.mjs index 7c320cd87..2b08e16b3 100644 --- a/scripts/gulpfiles/git_tasks.js +++ b/scripts/gulpfiles/git_tasks.mjs @@ -8,11 +8,11 @@ * @fileoverview Git-related gulp tasks for Blockly. */ -const gulp = require('gulp'); -const execSync = require('child_process').execSync; +import * as gulp from 'gulp'; +import {execSync} from 'child_process'; -const buildTasks = require('./build_tasks'); -const packageTasks = require('./package_tasks'); +import * as buildTasks from './build_tasks.mjs'; +import * as packageTasks from './package_tasks.mjs'; const UPSTREAM_URL = 'https://github.com/google/blockly.git'; @@ -63,7 +63,7 @@ function syncBranch(branchName) { * Stash current state, check out develop, and sync with * google/blockly. */ -function syncDevelop() { +export function syncDevelop() { return syncBranch('develop'); }; @@ -71,7 +71,7 @@ function syncDevelop() { * Stash current state, check out master, and sync with * google/blockly. */ -function syncMaster() { +export function syncMaster() { return syncBranch('master'); }; @@ -111,7 +111,7 @@ function checkoutBranch(branchName) { * Create and push an RC branch. * Note that this pushes to google/blockly. */ -const createRC = gulp.series( +export const createRC = gulp.series( syncDevelop(), function(done) { const branchName = getRCBranchName(); @@ -122,7 +122,7 @@ const createRC = gulp.series( ); /** Create the rebuild branch. */ -function createRebuildBranch(done) { +export function createRebuildBranch(done) { const branchName = getRebuildBranchName(); console.log(`make-rebuild-branch: creating branch ${branchName}`); execSync(`git switch -C ${branchName}`, { stdio: 'inherit' }); @@ -130,7 +130,7 @@ function createRebuildBranch(done) { } /** Push the rebuild branch to origin. */ -function pushRebuildBranch(done) { +export function pushRebuildBranch(done) { console.log('push-rebuild-branch: committing rebuild'); execSync('git commit -am "Rebuild"', { stdio: 'inherit' }); const branchName = getRebuildBranchName(); @@ -145,7 +145,7 @@ function pushRebuildBranch(done) { * * Prerequisites (invoked): clean, build. */ -const updateGithubPages = gulp.series( +export const updateGithubPages = gulp.series( function(done) { execSync('git stash save -m "Stash for sync"', { stdio: 'inherit' }); execSync('git switch -C gh-pages', { stdio: 'inherit' }); @@ -165,17 +165,3 @@ const updateGithubPages = gulp.series( done(); } ); - -module.exports = { - // Main sequence targets. Each should invoke any immediate prerequisite(s). - updateGithubPages, - - // Manually-invokable targets that invoke prerequisites. - createRC, - - // Legacy script-only targets, to be deleted. - syncDevelop, - syncMaster, - createRebuildBranch, - pushRebuildBranch, -}; diff --git a/scripts/gulpfiles/helper_tasks.js b/scripts/gulpfiles/helper_tasks.js deleted file mode 100644 index b239d03f5..000000000 --- a/scripts/gulpfiles/helper_tasks.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Any gulp helper functions. - */ - -// Clears the require cache to ensure the package.json is up to date. -function getPackageJson() { - delete require.cache[require.resolve('../../package.json')] - return require('../../package.json'); -} - -module.exports = { - getPackageJson: getPackageJson -} diff --git a/scripts/gulpfiles/helper_tasks.mjs b/scripts/gulpfiles/helper_tasks.mjs new file mode 100644 index 000000000..2068de106 --- /dev/null +++ b/scripts/gulpfiles/helper_tasks.mjs @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Any gulp helper functions. + */ + +import Module from "node:module"; + +const require = Module.createRequire(import.meta.url); + +/** + * Load and return the contents of package.json. + * + * Uses require() rather than import, and clears the require cache, to + * ensure the loaded package.json data is up to date. + */ +export function getPackageJson() { + delete require.cache[require.resolve('../../package.json')]; + return require('../../package.json'); +} + diff --git a/scripts/gulpfiles/package_tasks.js b/scripts/gulpfiles/package_tasks.mjs similarity index 89% rename from scripts/gulpfiles/package_tasks.js rename to scripts/gulpfiles/package_tasks.mjs index 89264a0e3..948f855b0 100644 --- a/scripts/gulpfiles/package_tasks.js +++ b/scripts/gulpfiles/package_tasks.mjs @@ -8,20 +8,17 @@ * @fileoverview Gulp tasks to package Blockly for distribution on NPM. */ -const gulp = require('gulp'); -gulp.concat = require('gulp-concat'); -gulp.replace = require('gulp-replace'); -gulp.rename = require('gulp-rename'); -gulp.insert = require('gulp-insert'); -gulp.umd = require('gulp-umd'); -gulp.replace = require('gulp-replace'); +import * as gulp from 'gulp'; +import concat from 'gulp-concat'; +import replace from 'gulp-replace'; +import umd from 'gulp-umd'; -const path = require('path'); -const fs = require('fs'); -const {rimraf} = require('rimraf'); -const build = require('./build_tasks'); -const {getPackageJson} = require('./helper_tasks'); -const {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TYPINGS_BUILD_DIR} = require('./config'); +import * as path from 'path'; +import * as fs from 'fs'; +import {rimraf} from 'rimraf'; +import * as build from './build_tasks.mjs'; +import {getPackageJson} from './helper_tasks.mjs'; +import {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TYPINGS_BUILD_DIR} from './config.mjs'; // Path to template files for gulp-umd. const TEMPLATE_DIR = 'scripts/package/templates'; @@ -32,7 +29,7 @@ const TEMPLATE_DIR = 'scripts/package/templates'; * @param {Array} dependencies An array of dependencies to inject. */ function packageUMD(namespace, dependencies, template = 'umd.template') { - return gulp.umd({ + return umd({ dependencies: function () { return dependencies; }, namespace: function () { return namespace; }, exports: function () { return namespace; }, @@ -88,7 +85,7 @@ function packageCoreNode() { function packageLocales() { // Remove references to goog.provide and goog.require. return gulp.src(`${LANG_BUILD_DIR}/*.js`) - .pipe(gulp.replace(/goog\.[^\n]+/g, '')) + .pipe(replace(/goog\.[^\n]+/g, '')) .pipe(packageUMD('Blockly.Msg', [], 'umd-msg.template')) .pipe(gulp.dest(`${RELEASE_DIR}/msg`)); }; @@ -107,7 +104,7 @@ function packageUMDBundle() { `${RELEASE_DIR}/javascript_compressed.js`, ]; return gulp.src(srcs) - .pipe(gulp.concat('blockly.min.js')) + .pipe(concat('blockly.min.js')) .pipe(gulp.dest(`${RELEASE_DIR}`)); }; @@ -140,7 +137,7 @@ function packageUMDBundle() { * @param {Function} done Callback to call when done. */ function packageLegacyEntrypoints(done) { - for (entrypoint of [ + for (const entrypoint of [ 'core', 'blocks', 'dart', 'javascript', 'lua', 'php', 'python' ]) { const bundle = @@ -218,14 +215,14 @@ function packageDTS() { .pipe(gulp.src(`${TYPINGS_BUILD_DIR}/**/*.d.ts`, {ignore: [ `${TYPINGS_BUILD_DIR}/blocks/**/*`, ]})) - .pipe(gulp.replace('AnyDuringMigration', 'any')) + .pipe(replace('AnyDuringMigration', 'any')) .pipe(gulp.dest(RELEASE_DIR)); }; /** * This task cleans the release directory (by deleting it). */ -function cleanReleaseDir() { +export function cleanReleaseDir() { // Sanity check. if (RELEASE_DIR === '.' || RELEASE_DIR === '/') { return Promise.reject(`Refusing to rm -rf ${RELEASE_DIR}`); @@ -237,9 +234,13 @@ function cleanReleaseDir() { * This task prepares the files to be included in the NPM by copying * them into the release directory. * + * This task was formerly called "package" but was renamed in + * preparation for porting gulpfiles to ESM because "package" is a + * reserved word. + * * Prerequisite: build. */ -const package = gulp.series( +export const pack = gulp.series( gulp.parallel( build.cleanBuildDir, cleanReleaseDir), @@ -254,9 +255,3 @@ const package = gulp.series( packageReadme, packageDTS) ); - -module.exports = { - // Main sequence targets. Each should invoke any immediate prerequisite(s). - cleanReleaseDir: cleanReleaseDir, - package: package, -}; diff --git a/scripts/gulpfiles/release_tasks.js b/scripts/gulpfiles/release_tasks.mjs similarity index 87% rename from scripts/gulpfiles/release_tasks.js rename to scripts/gulpfiles/release_tasks.mjs index f2545c7b9..a678a4f24 100644 --- a/scripts/gulpfiles/release_tasks.js +++ b/scripts/gulpfiles/release_tasks.mjs @@ -8,15 +8,15 @@ * @fileoverview Gulp scripts for releasing Blockly. */ -const execSync = require('child_process').execSync; -const fs = require('fs'); -const gulp = require('gulp'); -const readlineSync = require('readline-sync'); +import {execSync} from 'child_process'; +import * as fs from 'fs'; +import * as gulp from 'gulp'; +import * as readlineSync from 'readline-sync'; -const gitTasks = require('./git_tasks'); -const packageTasks = require('./package_tasks'); -const {getPackageJson} = require('./helper_tasks'); -const {RELEASE_DIR} = require('./config'); +import * as gitTasks from './git_tasks.mjs'; +import * as packageTasks from './package_tasks.mjs'; +import {getPackageJson} from './helper_tasks.mjs'; +import {RELEASE_DIR} from './config.mjs'; // Gets the current major version. @@ -147,17 +147,17 @@ function updateBetaVersion(done) { } // Rebuild, package and publish to npm. -const publish = gulp.series( - packageTasks.package, // Does clean + build. +export const publish = gulp.series( + packageTasks.pack, // Does clean + build. checkBranch, checkReleaseDir, loginAndPublish ); // Rebuild, package and publish a beta version of Blockly. -const publishBeta = gulp.series( +export const publishBeta = gulp.series( updateBetaVersion, - packageTasks.package, // Does clean + build. + packageTasks.pack, // Does clean + build. checkBranch, checkReleaseDir, loginAndPublishBeta @@ -165,19 +165,10 @@ const publishBeta = gulp.series( // Switch to a new branch, update the version number, build Blockly // and check in the resulting built files. -const recompileDevelop = gulp.series( +export const recompile = gulp.series( gitTasks.syncDevelop(), gitTasks.createRebuildBranch, updateVersionPrompt, - packageTasks.package, // Does clean + build. + packageTasks.pack, // Does clean + build. gitTasks.pushRebuildBranch ); - -module.exports = { - // Main sequence targets. Each should invoke any immediate prerequisite(s). - publishBeta, - publish, - - // Legacy target, to be deleted. - recompile: recompileDevelop, -}; diff --git a/scripts/gulpfiles/test_tasks.js b/scripts/gulpfiles/test_tasks.mjs similarity index 94% rename from scripts/gulpfiles/test_tasks.js rename to scripts/gulpfiles/test_tasks.mjs index 236a21d77..d4b73cdb3 100644 --- a/scripts/gulpfiles/test_tasks.js +++ b/scripts/gulpfiles/test_tasks.mjs @@ -9,19 +9,19 @@ */ /* eslint-env node */ -const asyncDone = require('async-done'); -const gulp = require('gulp'); -const gzip = require('gulp-gzip'); -const fs = require('fs'); -const path = require('path'); -const {execSync} = require('child_process'); -const {rimraf} = require('rimraf'); +import asyncDone from 'async-done'; +import * as gulp from 'gulp'; +import gzip from 'gulp-gzip'; +import * as fs from 'fs'; +import * as path from 'path'; +import {execSync} from 'child_process'; +import {rimraf} from 'rimraf'; -const {RELEASE_DIR, TEST_TSC_OUTPUT_DIR} = require('./config'); +import {RELEASE_DIR, TEST_TSC_OUTPUT_DIR} from './config.mjs'; -const {runMochaTestsInBrowser} = require('../../tests/mocha/webdriver.js'); -const {runGeneratorsInBrowser} = require('../../tests/generators/webdriver.js'); -const {runCompileCheckInBrowser} = require('../../tests/compile/webdriver.js'); +import {runMochaTestsInBrowser} from '../../tests/mocha/webdriver.js'; +import {runGeneratorsInBrowser} from '../../tests/generators/webdriver.js'; +import {runCompileCheckInBrowser} from '../../tests/compile/webdriver.js'; const OUTPUT_DIR = 'build/generators'; const GOLDEN_DIR = 'tests/generators/golden'; @@ -321,7 +321,7 @@ function checkResult(suffix) { * Run generator tests inside a browser and check the results. * @return {Promise} Asynchronous result. */ -async function generators() { +export async function generators() { return runTestTask('generators', async () => { // Clean up. rimraf.sync(OUTPUT_DIR); @@ -396,10 +396,6 @@ const tasks = [ advancedCompileInBrowser ]; -const test = gulp.series(...tasks, reportTestResult); +export const test = gulp.series(...tasks, reportTestResult); -module.exports = { - test, - generators, -}; diff --git a/tests/mocha/connection_checker_test.js b/tests/mocha/connection_checker_test.js index f353a2b77..fee2966d7 100644 --- a/tests/mocha/connection_checker_test.js +++ b/tests/mocha/connection_checker_test.js @@ -29,7 +29,10 @@ suite('Connection checker', function () { } test('Target Null', function () { - const connection = new Blockly.Connection({}, ConnectionType.INPUT_VALUE); + const connection = new Blockly.Connection( + {id: 'test'}, + ConnectionType.INPUT_VALUE, + ); assertReasonHelper( this.checker, connection, @@ -38,7 +41,7 @@ suite('Connection checker', function () { ); }); test('Target Self', function () { - const block = {workspace: 1}; + const block = {id: 'test', workspace: 1}; const connection1 = new Blockly.Connection( block, ConnectionType.INPUT_VALUE, @@ -57,11 +60,11 @@ suite('Connection checker', function () { }); test('Different Workspaces', function () { const connection1 = new Blockly.Connection( - {workspace: 1}, + {id: 'test1', workspace: 1}, ConnectionType.INPUT_VALUE, ); const connection2 = new Blockly.Connection( - {workspace: 2}, + {id: 'test2', workspace: 2}, ConnectionType.OUTPUT_VALUE, ); @@ -76,10 +79,10 @@ suite('Connection checker', function () { setup(function () { // We have to declare each separately so that the connections belong // on different blocks. - const prevBlock = {isShadow: function () {}}; - const nextBlock = {isShadow: function () {}}; - const outBlock = {isShadow: function () {}}; - const inBlock = {isShadow: function () {}}; + const prevBlock = {id: 'test1', isShadow: function () {}}; + const nextBlock = {id: 'test2', isShadow: function () {}}; + const outBlock = {id: 'test3', isShadow: function () {}}; + const inBlock = {id: 'test4', isShadow: function () {}}; this.previous = new Blockly.Connection( prevBlock, ConnectionType.PREVIOUS_STATEMENT, @@ -197,11 +200,13 @@ suite('Connection checker', function () { suite('Shadows', function () { test('Previous Shadow', function () { const prevBlock = { + id: 'test1', isShadow: function () { return true; }, }; const nextBlock = { + id: 'test2', isShadow: function () { return false; }, @@ -224,11 +229,13 @@ suite('Connection checker', function () { }); test('Next Shadow', function () { const prevBlock = { + id: 'test1', isShadow: function () { return false; }, }; const nextBlock = { + id: 'test2', isShadow: function () { return true; }, @@ -251,11 +258,13 @@ suite('Connection checker', function () { }); test('Prev and Next Shadow', function () { const prevBlock = { + id: 'test1', isShadow: function () { return true; }, }; const nextBlock = { + id: 'test2', isShadow: function () { return true; }, @@ -278,11 +287,13 @@ suite('Connection checker', function () { }); test('Output Shadow', function () { const outBlock = { + id: 'test1', isShadow: function () { return true; }, }; const inBlock = { + id: 'test2', isShadow: function () { return false; }, @@ -305,11 +316,13 @@ suite('Connection checker', function () { }); test('Input Shadow', function () { const outBlock = { + id: 'test1', isShadow: function () { return false; }, }; const inBlock = { + id: 'test2', isShadow: function () { return true; }, @@ -332,11 +345,13 @@ suite('Connection checker', function () { }); test('Output and Input Shadow', function () { const outBlock = { + id: 'test1', isShadow: function () { return true; }, }; const inBlock = { + id: 'test2', isShadow: function () { return true; }, @@ -373,9 +388,11 @@ suite('Connection checker', function () { }; test('Output connected, adding previous', function () { const outBlock = { + id: 'test1', isShadow: function () {}, }; const inBlock = { + id: 'test2', isShadow: function () {}, }; const outCon = new Blockly.Connection( @@ -394,6 +411,7 @@ suite('Connection checker', function () { ConnectionType.PREVIOUS_STATEMENT, ); const nextBlock = { + id: 'test3', isShadow: function () {}, }; const nextCon = new Blockly.Connection( @@ -410,9 +428,11 @@ suite('Connection checker', function () { }); test('Previous connected, adding output', function () { const prevBlock = { + id: 'test1', isShadow: function () {}, }; const nextBlock = { + id: 'test2', isShadow: function () {}, }; const prevCon = new Blockly.Connection( @@ -431,6 +451,7 @@ suite('Connection checker', function () { ConnectionType.OUTPUT_VALUE, ); const inBlock = { + id: 'test3', isShadow: function () {}, }; const inCon = new Blockly.Connection( @@ -449,8 +470,14 @@ suite('Connection checker', function () { }); suite('Check Types', function () { setup(function () { - this.con1 = new Blockly.Connection({}, ConnectionType.PREVIOUS_STATEMENT); - this.con2 = new Blockly.Connection({}, ConnectionType.NEXT_STATEMENT); + this.con1 = new Blockly.Connection( + {id: 'test1'}, + ConnectionType.PREVIOUS_STATEMENT, + ); + this.con2 = new Blockly.Connection( + {id: 'test2'}, + ConnectionType.NEXT_STATEMENT, + ); }); function assertCheckTypes(checker, one, two) { assert.isTrue(checker.doTypeChecks(one, two)); diff --git a/tests/mocha/connection_db_test.js b/tests/mocha/connection_db_test.js index e7f397d54..04f685124 100644 --- a/tests/mocha/connection_db_test.js +++ b/tests/mocha/connection_db_test.js @@ -5,6 +5,7 @@ */ import {ConnectionType} from '../../build/src/core/connection_type.js'; +import * as idGenerator from '../../build/src/core/utils/idgenerator.js'; import {assert} from '../../node_modules/chai/chai.js'; import { sharedTestSetup, @@ -31,7 +32,7 @@ suite('Connection Database', function () { }; workspace.connectionDBList[type] = opt_database || this.database; const connection = new Blockly.RenderedConnection( - {workspace: workspace}, + {id: idGenerator.getNextUniqueId(), workspace: workspace}, type, ); connection.x = x; diff --git a/tests/mocha/event_test.js b/tests/mocha/event_test.js index 00d704ff0..7423f22f7 100644 --- a/tests/mocha/event_test.js +++ b/tests/mocha/event_test.js @@ -355,6 +355,7 @@ suite('Events', function () { suite('With variable getter blocks', function () { setup(function () { + this.TEST_BLOCK_ID = 'test_block_id'; this.genUidStub = createGenUidStubWithReturns([ this.TEST_BLOCK_ID, 'test_var_id', diff --git a/tests/mocha/field_textinput_test.js b/tests/mocha/field_textinput_test.js index 82c1a645e..7dc105f72 100644 --- a/tests/mocha/field_textinput_test.js +++ b/tests/mocha/field_textinput_test.js @@ -294,4 +294,300 @@ suite('Text Input Fields', function () { this.assertValue('test text'); }); }); + + suite('Use editor', function () { + setup(function () { + this.blockJson = { + 'type': 'math_arithmetic', + 'id': 'test_arithmetic_block', + 'fields': { + 'OP': 'ADD', + }, + 'inputs': { + 'A': { + 'shadow': { + 'type': 'math_number', + 'id': 'left_input_block', + 'name': 'test_name', + 'fields': { + 'NUM': 1, + }, + }, + }, + 'B': { + 'shadow': { + 'type': 'math_number', + 'id': 'right_input_block', + 'fields': { + 'NUM': 2, + }, + }, + }, + }, + }; + + this.getFieldFromShadowBlock = function (shadowBlock) { + return shadowBlock.getFields().next().value; + }; + + this.simulateTypingIntoInput = (inputElem, newText) => { + // Typing into an input field changes its value directly and then fires + // an InputEvent (which FieldInput relies on to automatically + // synchronize its state). + inputElem.value = newText; + inputElem.dispatchEvent(new InputEvent('input')); + }; + }); + + // The block being tested doesn't use full-block fields in Geras. + suite('Geras theme', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv', { + renderer: 'geras', + }); + Blockly.serialization.blocks.append(this.blockJson, this.workspace); + + // The workspace actually needs to be visible for focus. + document.getElementById('blocklyDiv').style.visibility = 'visible'; + }); + teardown(function () { + document.getElementById('blocklyDiv').style.visibility = 'hidden'; + workspaceTeardown.call(this, this.workspace); + }); + + test('No editor open by default', function () { + // The editor is only opened if its indicated that it should be open. + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Type in editor with escape does not change field value', async function () { + const block = this.workspace.getBlockById('left_input_block'); + const field = this.getFieldFromShadowBlock(block); + field.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Change the value of the field's input through its editor. + const fieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(fieldEditor, 'updated value'); + fieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape', + }), + ); + + // 'Escape' will avoid saving the edited field value and close the editor. + assert.equal(field.getValue(), 1); + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Type in editor with enter changes field value', async function () { + const block = this.workspace.getBlockById('left_input_block'); + const field = this.getFieldFromShadowBlock(block); + field.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Change the value of the field's input through its editor. + const fieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(fieldEditor, '10'); + fieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // 'Enter' will save the edited result and close the editor. + assert.equal(field.getValue(), 10); + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Not finishing editing does not return ephemeral focus', async function () { + const block = this.workspace.getBlockById('left_input_block'); + const field = this.getFieldFromShadowBlock(block); + Blockly.getFocusManager().focusNode(field); + field.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Change the value of the field's input through its editor. + const fieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(fieldEditor, '10'); + + // If the editor doesn't restore focus then the current focused element is + // still the editor. + assert.strictEqual(document.activeElement, fieldEditor); + }); + + test('Finishing editing returns ephemeral focus', async function () { + const block = this.workspace.getBlockById('left_input_block'); + const field = this.getFieldFromShadowBlock(block); + Blockly.getFocusManager().focusNode(field); + field.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Change the value of the field's input through its editor. + const fieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(fieldEditor, '10'); + fieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape', + }), + ); + + // Verify that exiting the editor restores focus back to the field. + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), field); + assert.strictEqual(document.activeElement, field.getFocusableElement()); + }); + + test('Opening an editor, tabbing, then editing changes the second field', async function () { + const leftInputBlock = this.workspace.getBlockById('left_input_block'); + const rightInputBlock = + this.workspace.getBlockById('right_input_block'); + const leftField = this.getFieldFromShadowBlock(leftInputBlock); + const rightField = this.getFieldFromShadowBlock(rightInputBlock); + leftField.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Tab, then edit and close the editor. + document.querySelector('.blocklyHtmlInput').dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab', + }), + ); + const rightFieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(rightFieldEditor, '15'); + rightFieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // Verify that only the right field changed (due to the tab). + assert.equal(leftField.getValue(), 1); + assert.equal(rightField.getValue(), 15); + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Opening an editor, tabbing, then editing changes focus to the second field', async function () { + const leftInputBlock = this.workspace.getBlockById('left_input_block'); + const rightInputBlock = + this.workspace.getBlockById('right_input_block'); + const leftField = this.getFieldFromShadowBlock(leftInputBlock); + const rightField = this.getFieldFromShadowBlock(rightInputBlock); + Blockly.getFocusManager().focusNode(leftField); + leftField.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Tab, then edit and close the editor. + document.querySelector('.blocklyHtmlInput').dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab', + }), + ); + const rightFieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(rightFieldEditor, '15'); + rightFieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // Verify that the tab causes focus to change to the right field. + assert.strictEqual( + Blockly.getFocusManager().getFocusedNode(), + rightField, + ); + assert.strictEqual( + document.activeElement, + rightField.getFocusableElement(), + ); + }); + }); + + // The block being tested uses full-block fields in Zelos. + suite('Zelos theme', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv', { + renderer: 'zelos', + }); + Blockly.serialization.blocks.append(this.blockJson, this.workspace); + + // The workspace actually needs to be visible for focus. + document.getElementById('blocklyDiv').style.visibility = 'visible'; + }); + teardown(function () { + document.getElementById('blocklyDiv').style.visibility = 'hidden'; + workspaceTeardown.call(this, this.workspace); + }); + + test('Opening an editor, tabbing, then editing full block field changes the second field', async function () { + const leftInputBlock = this.workspace.getBlockById('left_input_block'); + const rightInputBlock = + this.workspace.getBlockById('right_input_block'); + const leftField = this.getFieldFromShadowBlock(leftInputBlock); + const rightField = this.getFieldFromShadowBlock(rightInputBlock); + leftField.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Tab, then edit and close the editor. + document.querySelector('.blocklyHtmlInput').dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab', + }), + ); + const rightFieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(rightFieldEditor, '15'); + rightFieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // Verify that only the right field changed (due to the tab). + assert.equal(leftField.getValue(), 1); + assert.equal(rightField.getValue(), 15); + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Opening an editor, tabbing, then editing full block field changes focus to the second field', async function () { + const leftInputBlock = this.workspace.getBlockById('left_input_block'); + const rightInputBlock = + this.workspace.getBlockById('right_input_block'); + const leftField = this.getFieldFromShadowBlock(leftInputBlock); + Blockly.getFocusManager().focusNode(leftInputBlock); + leftField.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Tab, then edit and close the editor. + document.querySelector('.blocklyHtmlInput').dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab', + }), + ); + const rightFieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(rightFieldEditor, '15'); + rightFieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // Verify that the tab causes focus to change to the right field block. + assert.strictEqual( + Blockly.getFocusManager().getFocusedNode(), + rightInputBlock, + ); + assert.strictEqual( + document.activeElement, + rightInputBlock.getFocusableElement(), + ); + }); + }); + }); }); diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index 3a1fc98a7..26dcb8dbe 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -249,6 +249,54 @@ suite('FocusManager', function () { // The second register should not fail since the tree was previously unregistered. }); + test('for tree with missing ID throws error', function () { + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + rootElem.removeAttribute('id'); + + const errorMsgRegex = + /Attempting to register a tree with a root element that has an invalid ID.+?/; + assert.throws( + () => this.focusManager.registerTree(this.testFocusableTree1), + errorMsgRegex, + ); + // Restore the ID for other tests. + rootElem.id = oldId; + }); + + test('for tree with null ID throws error', function () { + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + rootElem.setAttribute('id', null); + + const errorMsgRegex = + /Attempting to register a tree with a root element that has an invalid ID.+?/; + assert.throws( + () => this.focusManager.registerTree(this.testFocusableTree1), + errorMsgRegex, + ); + // Restore the ID for other tests. + rootElem.id = oldId; + }); + + test('for tree with empty throws error', function () { + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + rootElem.setAttribute('id', ''); + + const errorMsgRegex = + /Attempting to register a tree with a root element that has an invalid ID.+?/; + assert.throws( + () => this.focusManager.registerTree(this.testFocusableTree1), + errorMsgRegex, + ); + // Restore the ID for other tests. + rootElem.id = oldId; + }); + test('for unmanaged tree does not overwrite tab index', function () { this.focusManager.registerTree(this.testFocusableTree1, false); diff --git a/tests/mocha/focusable_tree_traverser_test.js b/tests/mocha/focusable_tree_traverser_test.js index 66cc598cc..0f88e1106 100644 --- a/tests/mocha/focusable_tree_traverser_test.js +++ b/tests/mocha/focusable_tree_traverser_test.js @@ -348,6 +348,80 @@ suite('FocusableTreeTraverser', function () { }); suite('findFocusableNodeFor()', function () { + test('for element without ID returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + // Normally it's not valid to miss an ID, but it can realistically happen. + rootElem.removeAttribute('id'); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + // Restore the ID for other tests. + rootElem.setAttribute('id', oldId); + + assert.isNull(finding); + }); + + test('for element with null ID returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + // Normally it's not valid to miss an ID, but it can realistically happen. + rootElem.setAttribute('id', null); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + // Restore the ID for other tests. + rootElem.setAttribute('id', oldId); + + assert.isNull(finding); + }); + + test('for element with null ID string returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + // This is a quirky version of the null variety above that's actually + // functionallity equivalent (since 'null' is converted to a string). + rootElem.setAttribute('id', 'null'); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + // Restore the ID for other tests. + rootElem.setAttribute('id', oldId); + + assert.isNull(finding); + }); + + test('for element with empty ID returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + // An empty ID is invalid since it will potentially conflict with other + // elements, and element IDs must be unique for focus management. + rootElem.setAttribute('id', ''); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + // Restore the ID for other tests. + rootElem.setAttribute('id', oldId); + + assert.isNull(finding); + }); + test('for root element returns root', function () { const tree = this.testFocusableTree1; const rootNode = tree.getRootFocusableNode(); diff --git a/tests/mocha/shortcut_items_test.js b/tests/mocha/shortcut_items_test.js index 4ab83d8e1..eaadef01e 100644 --- a/tests/mocha/shortcut_items_test.js +++ b/tests/mocha/shortcut_items_test.js @@ -434,13 +434,13 @@ suite('Keyboard Shortcut Items', function () { }); }); }); - // Do not undo if a gesture is in progress. - suite('Gesture in progress', function () { + // Do not undo if a drag is in progress. + suite('Drag in progress', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; const keyEvent = testCase[1]; test(testCaseName, function () { - sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + sinon.stub(this.workspace, 'isDragging').returns(true); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.notCalled(this.undoSpy); sinon.assert.notCalled(this.hideChaffSpy); @@ -494,13 +494,13 @@ suite('Keyboard Shortcut Items', function () { }); }); }); - // Do not undo if a gesture is in progress. - suite('Gesture in progress', function () { + // Do not redo if a drag is in progress. + suite('Drag in progress', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; const keyEvent = testCase[1]; test(testCaseName, function () { - sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + sinon.stub(this.workspace, 'isDragging').returns(true); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.notCalled(this.redoSpy); sinon.assert.notCalled(this.hideChaffSpy); @@ -534,8 +534,8 @@ suite('Keyboard Shortcut Items', function () { sinon.assert.calledWith(this.undoSpy, true); sinon.assert.calledOnce(this.hideChaffSpy); }); - test('Not called when a gesture is in progress', function () { - sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + test('Not called when a drag is in progress', function () { + sinon.stub(this.workspace, 'isDragging').returns(true); this.injectionDiv.dispatchEvent(this.ctrlYEvent); sinon.assert.notCalled(this.undoSpy); sinon.assert.notCalled(this.hideChaffSpy);