diff --git a/CHANGELOG.md b/CHANGELOG.md index 865317e7..e8f72dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Fixed a bug where the `Download JSON` button would not download the JSON currently inside the input box. - Made sure file input would reset when file input dialog was closed. +- Fixed a bug where the styles in `DisplaySettings.roughjs_config` were not applied. ### 📚 Documentation and demo website changes @@ -27,6 +28,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### 🔧 Internal changes +- Added better typing. + ## [0.3.2] - 2024-09-14 ### ✨ Enhancements diff --git a/docs/.gitignore b/docs/.gitignore index b2d6de30..5f222912 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -7,6 +7,7 @@ # Generated files .docusaurus .cache-loader +/docs/api # Misc .DS_Store diff --git a/docs/docs/99-api/_category_.yml b/docs/docs/99-api/_category_.yml deleted file mode 100644 index c5d5974f..00000000 --- a/docs/docs/99-api/_category_.yml +++ /dev/null @@ -1 +0,0 @@ -label: "API" diff --git a/docs/docs/99-api/modules.md b/docs/docs/99-api/modules.md deleted file mode 100644 index 49fa5e04..00000000 --- a/docs/docs/99-api/modules.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -id: "modules" -title: "memory-viz" -sidebar_label: "Exports" -sidebar_position: 0.5 -custom_edit_url: null ---- - -## Functions - -### draw - -▸ **draw**(`objects`, `automation`, `configuration`): `any` - -Draw the given objects on the canvas. - -The format of the array of objects must adhere to the description provided in MemoryModel.drawAll. - -#### Parameters - -| Name | Type | Description | -| :-------------- | :---- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `objects` | `any` | The array of objects to be drawn: this could be passed as an actual JavaScript array of objects, or as a JSON file containing the object array. This array of objects may also include the user-defined style configuration. See the demo files and style.md file for details. | -| `automation` | `any` | Whether the coordinates (of the objects on the canvas) should be automatically generated or manually inputted. | -| `configuration` | `any` | The configuration (display settings) defined by the user. This is also the place to define `sort_by` ("height" or "id") for the object space. NOTE: In the case that automation == true, then the user must define configuration.width, as this will be used as the "max canvas width" for the automation process. If this user-defined width is too small, it will be overwritten by a calculated minimum width value. If automation == false, then all configuration properties are optional, and the function will still operate even without defining them. | - -#### Returns - -`any` - -the produced canvas - -#### Defined in - -[user_functions.ts:29](https://github.com/david-yz-liu/memory-viz/blob/442d14c/memory-viz/src/user_functions.ts#L29) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index e40bd058..912522c3 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -145,7 +145,6 @@ const config = { entryPoints: ["../memory-viz/src/user_functions.ts"], readme: "none", tsconfig: "../tsconfig.json", - out: "99-api", }, ], ], diff --git a/memory-viz/package.json b/memory-viz/package.json index 06f2c73e..2a21f794 100644 --- a/memory-viz/package.json +++ b/memory-viz/package.json @@ -54,6 +54,7 @@ "@types/jest": "^29.5.12", "@types/node": "^20.10.5", "babel-loader": "^9.1.0", + "csstype": "^3.1.3", "husky": "^9.1.5", "jest": "^29.7.0", "tmp": "^0.2.3", diff --git a/memory-viz/src/automate.ts b/memory-viz/src/automate.ts index 9e368030..9c5ec2b7 100644 --- a/memory-viz/src/automate.ts +++ b/memory-viz/src/automate.ts @@ -1,17 +1,21 @@ import { MemoryModel } from "./memory_model"; import { config } from "./config"; -import { DrawnEntity } from "./types"; +import { DisplaySettings, DrawnEntity, Size, SortOptions } from "./types"; /** * Draws the objects given in the path in an automated fashion. * - * @param {DrawnEntity[]} objects - The list of objects that will be drawn on the canvas. - * @param {Object} configuration - The configuration settings defined by the user. - * @param {number} width - User-defined width of the canvas. - * @returns {MemoryModel} - The memory model that is created according to the objects given in the path (the JSON + * @param objects - The list of objects that will be drawn on the canvas. + * @param configuration - The configuration settings defined by the user. + * @param width - User-defined width of the canvas. + * @returns - The memory model that is created according to the objects given in the path (the JSON * file) */ -function drawAutomated(objects: DrawnEntity[], width, configuration) { +function drawAutomated( + objects: DrawnEntity[], + width: number, + configuration: Partial +): MemoryModel { const { stack_frames, other_items } = separateObjects(objects); // Assigning the objects with coordinates. @@ -20,7 +24,7 @@ function drawAutomated(objects: DrawnEntity[], width, configuration) { // Determining the minimum width of the canvas. let min_width = 0; - let item_width; + let item_width: number; for (const item of other_items) { item_width = getSize(item).width; if (item_width > min_width) { @@ -65,15 +69,22 @@ function drawAutomated(objects: DrawnEntity[], width, configuration) { * height for drawing the stack-frames. The returned collection of stack-frames is the augmented version * of the input such that the x and y coordinates of the stack-frames are determined automatically. * - * @param {Object} configuration - The configuration set by the user. - * @param {DrawnEntity[]} stack_frames - The list of stack-frames that will be drawn + * @param configuration - The configuration set by the user. + * @param stack_frames - The list of stack-frames that will be drawn * (without the specified x and y coordinates) - * @returns {Object} - Returns the object consisting of three attributes as follows: stack-frames which will be drawn, + * @returns - Returns the object consisting of three attributes as follows: stack-frames which will be drawn, * the minimum required height of the canvas for drawing stack frames and required width for drawing all the stack * frames. Notably, the last two attributes will be useful in terms of dynamically deciding the width and the height * of the canvas. */ -function drawAutomatedStackFrames(stack_frames: DrawnEntity[], configuration) { +function drawAutomatedStackFrames( + stack_frames: DrawnEntity[], + configuration: Partial +): { + StackFrames: DrawnEntity[]; + requiredHeight: number; + requiredWidth: number; +} { for (const req_prop of [ "padding", "top_margin", @@ -93,8 +104,8 @@ function drawAutomatedStackFrames(stack_frames: DrawnEntity[], configuration) { let draw_stack_frames = []; for (const stack_frame of stack_frames) { - let width; - let height; + let width: number; + let height: number; if (stack_frame.type !== ".blank-frame") { const size = getSize(stack_frame); @@ -135,22 +146,22 @@ function drawAutomatedStackFrames(stack_frames: DrawnEntity[], configuration) { * desired canvas width, this function mutates the passed list to equip each object with coordinates (corresponding to * the top-left corner of the object's box in the canvas). * - * @param {DrawnEntity[]} objs - list of objects in the format described in MemoryModel.drawAll - * @param {number} max_width - the desired width of the canvas - * @param {*} sort_by - the sorting criterion; must be "height" or "id", otherwise no sorting takes place. - * @param {object} config_aut - additional configuration options, such as margins, paddings, e.t.c. - * @param {number} sf_endpoint - the x-coordinate of the right edge of the stackframe column; this will determine + * @param objs - list of objects in the format described in MemoryModel.drawAll + * @param max_width - the desired width of the canvas + * @param sort_by - the sorting criterion; must be "height" or "id", otherwise no sorting takes place. + * @param config_aut - additional configuration options, such as margins, paddings, e.t.c. + * @param sf_endpoint - the x-coordinate of the right edge of the stackframe column; this will determine * where the object space begins. - * @returns {object} the mutates list of objects (where each object is now equipped with x-y coordinates) and the + * @returns the mutates list of objects (where each object is now equipped with x-y coordinates) and the * dynamically determined height the canvas will need to be. */ function drawAutomatedOtherItems( objs: DrawnEntity[], - max_width, - sort_by, - config_aut: any = {} /* to avoid undefined error */, - sf_endpoint -) { + max_width: number, + sort_by: SortOptions, + config_aut: Partial, + sf_endpoint: number +): { objs: DrawnEntity[]; canvas_height: number; canvas_width: number } { for (const req_prop of [ "padding", "top_margin", @@ -184,15 +195,15 @@ function drawAutomatedOtherItems( * This "compare" function is created and assigned to the variable 'compareFunc' in the following switch statement. * @param a - an object in objs * @param b - another object in objs - * @returns {number} negative if 'a' is taller, 0 if they have the same height, and positive if 'b' is taller. + * @returns negative if 'a' is taller, 0 if they have the same height, and positive if 'b' is taller. */ - let compareFunc; + let compareFunc: (a: DrawnEntity, b: DrawnEntity) => number; switch (sort_by) { - case "height": + case SortOptions.Height: compareFunc = compareByHeight; break; - case "id": + case SortOptions.Id: compareFunc = compareByID; break; } @@ -205,7 +216,7 @@ function drawAutomatedOtherItems( let y_coord = config_aut.top_margin; // Once a row is occupied, we must establish its height to determine the y-coordinate of the next row's boxes. - let row_height; + let row_height: number; let curr_row_objects = []; for (const item of objs) { let hor_reach = x_coord + item.width + PADDING; @@ -267,11 +278,14 @@ function drawAutomatedOtherItems( * The returned object has two attributes as 'stack_frames' and 'other_items'. * Each of these attributes are a list of objects that were originally given by the user. * - * @param {DrawnEntity[]} objects - The list of objects, including stack-frames (if any) and other items, that + * @param objects - The list of objects, including stack-frames (if any) and other items, that * will be drawn - * @returns {object} an object separating between stack-frames and the rest of the items. + * @returns an object separating between stack-frames and the rest of the items. */ -function separateObjects(objects: DrawnEntity[]) { +function separateObjects(objects: DrawnEntity[]): { + stack_frames: DrawnEntity[]; + other_items: DrawnEntity[]; +} { let stackFrames = []; let otherItems = []; @@ -301,10 +315,10 @@ function separateObjects(objects: DrawnEntity[]) { * Return the dimensions that the passed object will have if drawn on a canvas (in the context of the MemoryModel class). * This function can be used to determine how much space an object box will take on canvas (like a dry-run), given the * implementations of the 'draw' methods in MemoryModel. - * @param {DrawnEntity} obj - an object as specified in MemoryModel.drawAll, except that coordinates are missing. - * @returns {object} the width and the height the drawn object would have. + * @param obj - an object as specified in MemoryModel.drawAll, except that coordinates are missing. + * @returns the width and the height the drawn object would have. */ -function getSize(obj: DrawnEntity) { +function getSize(obj: DrawnEntity): Size { // The x and y values here are unimportant; 'obj' must simply have these properties for processing by 'drawAll'. obj.x = obj.x || 10; obj.y = obj.y || 10; @@ -323,9 +337,9 @@ function getSize(obj: DrawnEntity) { * function, it will prioritize 'a' over 'b'), 0 if they are equally tall, and positive if 'b' is taller. * @param a - an object * @param b - another object - * @returns {number} negative if 'a' is taller, 0 if they have the same height, and positive if 'b' is taller. + * @returns negative if 'a' is taller, 0 if they have the same height, and positive if 'b' is taller. */ -function compareByHeight(a, b) { +function compareByHeight(a: DrawnEntity, b: DrawnEntity): number { return -(a.height - b.height); } @@ -336,9 +350,9 @@ function compareByHeight(a, b) { * and positive if 'b.id' is larger. * @param a - an object * @param b - another object - * @returns {number} negative if 'a.id' is larger, 0 if a.id == b.id, and positive if 'b.id' is larger. + * @returns negative if 'a.id' is larger, 0 if a.id == b.id, and positive if 'b.id' is larger. */ -function compareByID(a, b) { +function compareByID(a: DrawnEntity, b: DrawnEntity): number { return a.id - b.id; } @@ -348,9 +362,9 @@ function compareByID(a, b) { * 'b' is righter. * @param a - an object * @param b - another object - * @returns {number} negative if 'a' is righter, 0 if 'a' and 'b' are equally right, and positive if b' is righter. + * @returns negative if 'a' is righter, 0 if 'a' and 'b' are equally right, and positive if b' is righter. */ -function compareByRightness(a, b) { +function compareByRightness(a: DrawnEntity, b: DrawnEntity): number { const a_right_edge = a.x + a.width; const b_right_edge = b.x + b.width; return -(a_right_edge - b_right_edge); @@ -362,9 +376,9 @@ function compareByRightness(a, b) { * 'b' is bottomer. * @param a - an object * @param b - another object - * @returns {number} negative if 'a' is bottomer, 0 if 'a' and 'b' are equally bottom, and positive if b' is bottomer. + * @returns negative if 'a' is bottomer, 0 if 'a' and 'b' are equally bottom, and positive if b' is bottomer. */ -function compareByBottomness(a, b) { +function compareByBottomness(a: DrawnEntity, b: DrawnEntity): number { const a_bottom_edge = a.y + a.height; const b_bottom_edge = b.y + b.height; return -(a_bottom_edge - b_bottom_edge); diff --git a/memory-viz/src/config.ts b/memory-viz/src/config.ts index 93da63ec..55b85f72 100644 --- a/memory-viz/src/config.ts +++ b/memory-viz/src/config.ts @@ -1,4 +1,6 @@ -export const config = { +import { VisualizationConfig } from "./types"; + +export const config: Partial = { rect_style: { stroke: "rgb(0, 0, 0)" }, text_color: "rgb(0, 0, 0)", // Default text color value_color: "rgb(27, 14, 139)", // Text color for primitive values diff --git a/memory-viz/src/memory_model.ts b/memory-viz/src/memory_model.ts index bb31fb8b..d5fba9c4 100644 --- a/memory-viz/src/memory_model.ts +++ b/memory-viz/src/memory_model.ts @@ -5,10 +5,21 @@ import merge from "deepmerge"; import { collections, immutable, presets, setStyleSheet } from "./style"; import { config } from "./config"; import { DOMImplementation, XMLSerializer } from "@xmldom/xmldom"; -import { DrawnEntity } from "./types"; +import { + DrawnEntity, + VisualizationConfig, + Rect, + Primitive, + Style, +} from "./types"; +import { isArrayOfType } from "./typeguards"; +import { RoughSVG } from "roughjs/bin/svg"; +import { Config, Options } from "roughjs/bin/core"; +import type * as fsType from "fs"; +import type * as CSS from "csstype"; // Dynamic import of Node fs module -let fs; +let fs: typeof fsType | undefined; if (typeof window === "undefined") { fs = require("fs"); } @@ -17,9 +28,9 @@ if (typeof window === "undefined") { export class MemoryModel { /** * Create the memory model diagram. - * @property {object} svg - An svg 'Element' object from the DOM (Document Object Model) module. + * @property svg - An svg 'Element' object from the DOM (Document Object Model) module. * Scalable Vector Graphics (svg) is an image format based on geometry. - * @property {object} rough_svg - Instantiating a RoughSVG object by passing the root svg node (this.svg) to the + * @property rough_svg - Instantiating a RoughSVG object by passing the root svg node (this.svg) to the * 'rough.svg()' method. As per the documentation of the 'rough' library, * "RoughSVG provides the main interface to work with this library". * @@ -32,8 +43,8 @@ export class MemoryModel { */ document: Document; svg: SVGSVGElement; - rough_svg: any; - rect_style: object; + rough_svg: RoughSVG; + rect_style: Options; text_color: string; // Default text color value_color: string; // Text color for primitive values id_color: string; // Text color for object ids @@ -48,10 +59,9 @@ export class MemoryModel { list_index_sep: number; // Vertical offset for list index labels font_size: number; // Font size, in px browser: boolean; // Whether this library is being used in a browser context - roughjs_config: object; // Configuration object used to pass in options to rough.js + roughjs_config: Config; // Configuration object used to pass in options to rough.js - constructor(options?: any) { - options = options || {}; + constructor(options: Partial = {}) { if (options.browser) { this.document = document; } else { @@ -67,8 +77,14 @@ export class MemoryModel { "svg" ); - this.svg.setAttribute("width", options.width || 800); - this.svg.setAttribute("height", options.height || 800); + this.svg.setAttribute( + "width", + options.width ? options.width.toString() : "800" + ); + this.svg.setAttribute( + "height", + options.height ? options.height.toString() : "800" + ); this.roughjs_config = options.roughjs_config; this.rough_svg = rough.svg(this.svg, this.roughjs_config); @@ -85,9 +101,9 @@ export class MemoryModel { /** * Serialize the generated SVG element into a readable string. * - * @returns {String} a readable string for the generated SVG element + * @returns a readable string for the generated SVG element */ - serializeSVG(): String { + serializeSVG(): string { const xmlSerializer = new XMLSerializer(); return xmlSerializer.serializeToString(this.svg); } @@ -98,12 +114,12 @@ export class MemoryModel { * @param path - The repository (local location that the image * will be saved). */ - save(path) { + save(path: string): void { const xml = this.serializeSVG(); if (path === undefined) { console.log(xml); } else { - fs.writeFile(path, xml, (err) => { + fs.writeFile(path, xml, (err: Error) => { if (err) { console.error(err); } @@ -115,7 +131,7 @@ export class MemoryModel { * Render the image (show the output) SVG to a given canvas object. * @param canvas - the element that will be used to draw graphics */ - render(canvas) { + render(canvas: HTMLCanvasElement): void { let image = new Image(); let data = "data:image/svg+xml;base64," + window.btoa(this.svg.toString()); @@ -130,30 +146,44 @@ export class MemoryModel { * Clear a given canvas object. * @param canvas - the element that is currently used to draw graphics */ - clear(canvas) { + clear(canvas: HTMLCanvasElement): void { const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); } /** * Distribute the object drawing depending on type - * @param {number} x - value for x coordinate of top left corner - * @param {number} y - value for y coordinate of top left corner - * @param {string} type - the data type (e.g. list, int) of the object we want draw - * @param {number} id - the hypothetical memory address number - * @param {*} value - can be passed as a list if type is a collection type - * @param {boolean} show_indexes - whether to show list indices - * @param {Object} style - The style configuration for the drawings on the canvas (e.g. highlighting, bold texts) + * @param x - value for x coordinate of top left corner + * @param y - value for y coordinate of top left corner + * @param type - the data type (e.g. list, int) of the object we want draw + * @param id - the hypothetical memory address number + * @param value - can be passed as a list if type is a collection type + * @param show_indexes - whether to show list indices + * @param style - The style configuration for the drawings on the canvas (e.g. highlighting, bold texts) * For style, firstly refer to `style.md` and `presets.md`. For the styling options in terms of texts, refer to * the SVG documentation. For the styling options in terms of boxes, refer to the Rough.js documentation. */ - drawObject(x, y, type, id, value, show_indexes, style) { + drawObject( + x: number, + y: number, + type: string, + id: number, + value: object | number[] | string | boolean | null, + show_indexes: boolean, + style: Style + ): Rect { if (collections.includes(type)) { - if (type === "dict") { + if (type === "dict" && typeof value === "object") { return this.drawDict(x, y, id, value, style); - } else if (type === "set") { + } else if ( + type === "set" && + isArrayOfType(value, "number") + ) { return this.drawSet(x, y, id, value, style); - } else if (type === "list" || type === "tuple") { + } else if ( + (type === "list" || type === "tuple") && + isArrayOfType(value, "number") + ) { return this.drawSequence( x, y, @@ -165,22 +195,34 @@ export class MemoryModel { ); } } else { - return this.drawPrimitive(x, y, type, id, value, style); + if (typeof value !== "object") { + return this.drawPrimitive(x, y, type, id, value, style); + } } + throw new Error( + `Invalid type or value: Expected a collection type (dict, set, list, tuple) or a primitive value, but received type "${type}" with value "${value}".` + ); } /** * Draw a primitive object. - * @param {number} x - value for x coordinate of top left corner - * @param {number} y - value for y coordinate of top left corner - * @param {string} type - the primitive data type (e.g. boolean, int) of the object we want draw - * @param {number} id - the hypothetical memory address number - * @param {*} value - the value of the primitive object - * @param {Object} style - The style configuration for the drawings on the canvas (e.g. highlighting, bold texts) + * @param x - value for x coordinate of top left corner + * @param y - value for y coordinate of top left corner + * @param type - the primitive data type (e.g. boolean, int) of the object we want draw + * @param id - the hypothetical memory address number + * @param value - the value of the primitive object + * @param style - The style configuration for the drawings on the canvas (e.g. highlighting, bold texts) * For the styling options in terms of texts, refer to the SVG documentation. For the styling options in terms of * boxes, refer to the Rough.js documentation. */ - drawPrimitive(x, y, type, id, value, style) { + drawPrimitive( + x: number, + y: number, + type: string, + id: number, + value: Primitive, + style: Style + ): Rect { let box_width = Math.max( this.obj_min_width, this.getTextLength(String(value)) + this.obj_x_padding @@ -193,7 +235,7 @@ export class MemoryModel { style.box_container ); - let size = { + let size: Rect = { width: box_width, height: this.obj_min_height, x: x, @@ -216,7 +258,7 @@ export class MemoryModel { }; } - let display_text; + let display_text: string; if (type === "bool") { display_text = value ? "True" : "False"; } else if (type === "str") { @@ -242,16 +284,23 @@ export class MemoryModel { /** * Draw the id and type properties of an object with a given type and id. - * @param {number} id - the hypothetical memory address number - * @param {string} type - the data type of the given object - * @param {number} x - value for x coordinate of top left corner - * @param {number} y - value for y coordinate of top left corner - * @param {number} width - The width of the given box (rectangle) - * @param {Object} style - The style configuration for the drawings on the canvas (e.g. highlighting, bold texts) + * @param id - the hypothetical memory address number + * @param type - the data type of the given object + * @param x - value for x coordinate of top left corner + * @param y - value for y coordinate of top left corner + * @param width - The width of the given box (rectangle) + * @param style - The style configuration for the drawings on the canvas (e.g. highlighting, bold texts) * For the styling options in terms of texts, refer to the SVG documentation. For the styling options in terms of * boxes, refer to the Rough.js documentation. */ - drawProperties(id, type, x, y, width, style) { + drawProperties( + id: number, + type: string, + x: number, + y: number, + width: number, + style: Style + ) { let id_box = Math.max( this.prop_min_width, this.getTextLength(`id${id}`) + 10 @@ -290,11 +339,11 @@ export class MemoryModel { /** * Draw a sequence object (must be either a list or a tuple). - * @param {number} x - value for x coordinate of top left corner - * @param {number} y - value for y coordinate of top left corner - * @param {string} type - the data type of the given object (tuple or list) - * @param {number} id - the hypothetical memory address number - * @param {number[]} element_ids - the list of id's corresponding to the values stored in this set. + * @param x - value for x coordinate of top left corner + * @param y - value for y coordinate of top left corner + * @param type - the data type of the given object (tuple or list) + * @param id - the hypothetical memory address number + * @param element_ids - the list of id's corresponding to the values stored in this set. * NOTE: * 1. This argument MUST be an array, since the built-in 'forEach' method works only for * (finite) ordered collections (i.e. with indexing). Sets are a type of unordered collection. @@ -302,8 +351,8 @@ export class MemoryModel { * If the instructor wishes to showcase the corresponding values, it is their responsibility to create * memory boxes for all elements (with id's that match the id's held in 'element_ids'). * - * @param {boolean} show_idx - whether to show the indexes of each list element - * @param {object} style - object defining the desired style of the sequence. As described in the docstring of + * @param show_idx - whether to show the indexes of each list element + * @param style - object defining the desired style of the sequence. As described in the docstring of * 'drawAll', this must be in the form * {text: * {value: {...}, id : {...}, type : {...}}, @@ -313,11 +362,19 @@ export class MemoryModel { * Moreover, note that this program does not force that for every id in the element_ids argument there is * a corresponding object (and its memory box) in our canvas. * - * @param {Object} style - The style configuration for the drawings on the canvas (e.g. highlighting, bold texts) + * @param style - The style configuration for the drawings on the canvas (e.g. highlighting, bold texts) * For the styling options in terms of texts, refer to the SVG documentation. For the styling options in terms of * boxes, refer to the Rough.js documentation. */ - drawSequence(x, y, type, id, element_ids, show_idx, style) { + drawSequence( + x: number, + y: number, + type: string, + id: number, + element_ids: number[], + show_idx: boolean, + style: Style + ): Rect { let box_width = this.obj_x_padding * 2; element_ids.forEach((v) => { @@ -336,7 +393,7 @@ export class MemoryModel { this.drawRect(x, y, box_width, box_height, style.box_container); - const size = { width: box_width, height: box_height, x: x, y: y }; + const size: Rect = { width: box_width, height: box_height, x: x, y: y }; if (immutable.includes(type)) { this.drawRect( @@ -374,7 +431,7 @@ export class MemoryModel { ); if (show_idx) { this.drawText( - i, + i.toString(), curr_x + item_length / 2, item_y - this.item_min_height / 4, style.text_id, @@ -396,25 +453,31 @@ export class MemoryModel { /** * Draw a set object. - * @param {number} x - value for x coordinate of top left corner - * @param {number} y - value for y coordinate of top left corner - * @param {number} id - the hypothetical memory address number - * @param {number[]} element_ids - the list of id's corresponding to the values stored in this set. + * @param x - value for x coordinate of top left corner + * @param y - value for y coordinate of top left corner + * @param id - the hypothetical memory address number + * @param element_ids - the list of id's corresponding to the values stored in this set. * NOTE: * 1. This argument MUST be an array, since the built-in 'forEach' method works only for * (finite) ordered collections (i.e. with indexing). Sets are a type of unordered collection. * 2. The 'element_ids' argument must store the id's and not the actual value of the list elements. * If the instructor wishes to showcase the corresponding values, it is their responsibility to create * memory boxes for all elements (with id's that match the id's held in 'element_ids'). - * @param {object} style - object defining the desired style of the sequence. Must abide by the structure defined + * @param style - object defining the desired style of the sequence. Must abide by the structure defined * in 'drawAll'. * * Moreover, note that this program does not force that for every id in the element_ids argument there is * a corresponding object (and its memory box) in our canvas. * - * @returns {number[]} the top-left coordinates, width, and height of the outermost box + * @returns the top-left coordinates, width, and height of the outermost box */ - drawSet(x, y, id, element_ids, style) { + drawSet( + x: number, + y: number, + id: number, + element_ids: number[], + style: Style + ): Rect { let box_width = this.obj_x_padding * 2; element_ids.forEach((v) => { box_width += Math.max( @@ -433,7 +496,12 @@ export class MemoryModel { style.box_container ); - const SIZE = { x, y, width: box_width, height: this.obj_min_height }; + const SIZE: Rect = { + x, + y, + width: box_width, + height: this.obj_min_height, + }; let curr_x = x + this.item_min_width / 2; let item_y = @@ -493,16 +561,22 @@ export class MemoryModel { /** * Draw a dictionary object - * @param {number} x - value for x coordinate of top left corner - * @param {number} y - value for y coordinate of top left corner - * @param {number} id - the hypothetical memory address number - * @param {object} obj - the object that will be drawn - * @param {object} style - object defining the desired style of the sequence. Must abide by the structure defined + * @param x - value for x coordinate of top left corner + * @param y - value for y coordinate of top left corner + * @param id - the hypothetical memory address number + * @param obj - the object that will be drawn + * @param style - object defining the desired style of the sequence. Must abide by the structure defined * in 'drawAll'. * - * @returns {object} the top-left coordinates, width, and height of the outermost box + * @returns the top-left coordinates, width, and height of the outermost box */ - drawDict(x, y, id, obj, style) { + drawDict( + x: number, + y: number, + id: number, + obj: object, + style: Style + ): Rect { let box_width = this.obj_min_width; let box_height = this.prop_min_height + this.item_min_height / 2; @@ -549,7 +623,7 @@ export class MemoryModel { } this.drawRect(x, y, box_width, box_height, style.box_container); - const SIZE = { x, y, width: box_width, height: box_height }; + const SIZE: Rect = { x, y, width: box_width, height: box_height }; // A second loop, so that we can position the colon and value boxes correctly. curr_y = y + this.prop_min_height + this.item_min_height / 2; @@ -595,18 +669,26 @@ export class MemoryModel { /** * Draw a custom class. - * @param {number} x - value for x coordinate of top left corner - * @param {number} y - value for y coordinate of top left corner - * @param {string} name - the name of the class - * @param {string} id - the hypothetical memory address number - * @param {object} attributes - the attributes of the given class - * @param {boolean} stack_frame - set to true if you are drawing a stack frame - * @param {object} style - object defining the desired style of the sequence. Must abide by the structure defined + * @param x - value for x coordinate of top left corner + * @param y - value for y coordinate of top left corner + * @param name - the name of the class + * @param id - the hypothetical memory address number + * @param attributes - the attributes of the given class + * @param stack_frame - set to true if you are drawing a stack frame + * @param style - object defining the desired style of the sequence. Must abide by the structure defined * in 'drawAll'. * - * @returns {number[]} the top-left coordinates, width, and height of the outermost box + * @returns the top-left coordinates, width, and height of the outermost box */ - drawClass(x, y, name, id, attributes, stack_frame, style) { + drawClass( + x: number, + y: number, + name: string, + id: number, + attributes: object, + stack_frame: boolean, + style: Style + ): Rect { let box_width = this.obj_min_width; let longest = 0; for (const attribute in attributes) { @@ -632,7 +714,7 @@ export class MemoryModel { } this.drawRect(x, y, box_width, box_height, style.box_container); - const SIZE = { x, y, width: box_width, height: box_height }; + const SIZE: Rect = { x, y, width: box_width, height: box_height }; // Draw element boxes. let curr_y = y + this.prop_min_height + this.item_min_height / 2; @@ -692,11 +774,11 @@ export class MemoryModel { /** * Draw a rectangle that will be used to represent the objects. - * @param {number} x - value for x coordinate of top left corner - * @param {number} y - value for y coordinate of top left corner - * @param {number} width - the width of the rectangle - * @param {number} height - the height of the rectangle - * @param {object | undefined} style - 1-D object with style properties for a Rough.js object, as per the + * @param x - value for x coordinate of top left corner + * @param y - value for y coordinate of top left corner + * @param width - the width of the rectangle + * @param height - the height of the rectangle + * @param style - 1-D object with style properties for a Rough.js object, as per the * Rough.js API. For instance, {fill: 'blue', stroke: 'red'}. */ drawRect( @@ -704,13 +786,13 @@ export class MemoryModel { y: number, width: number, height: number, - style?: object - ) { + style?: Options + ): void { if (style === undefined) { style = this.rect_style; } - style = { ...style, config: this.roughjs_config }; + style = { ...style, ...this.roughjs_config?.options }; this.svg.appendChild( this.rough_svg.rectangle(x, y, width, height, style) @@ -719,24 +801,30 @@ export class MemoryModel { /** * Draw given text - * @param {string} text - The text message that will be displayed - * @param {number} x - value for x coordinate of top left corner - * @param {number} y - value for y coordinate of top left corner - * @param {Object} style - 1-D object with style properties for a svg object, as per the + * @param text - The text message that will be displayed + * @param x - value for x coordinate of top left corner + * @param y - value for y coordinate of top left corner + * @param style - 1-D object with style properties for a svg object, as per the * standard SVG attributes, documented on * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text. * For instance, {fill: 'blue', stroke: 'red'} - * @param {string} text_class - The CSS class (if any) of the text message to be drawn + * @param text_class - The CSS class (if any) of the text message to be drawn */ - drawText(text, x, y, style, text_class = undefined) { + drawText( + text: string, + x: number, + y: number, + style: CSS.Properties, + text_class: string = undefined + ): void { const newElement = this.document.createElementNS( "http://www.w3.org/2000/svg", "text" ); - newElement.setAttribute("x", x); - newElement.setAttribute("y", y); + newElement.setAttribute("x", x.toString()); + newElement.setAttribute("y", y.toString()); if (style !== undefined) { let new_style = ""; @@ -757,47 +845,47 @@ export class MemoryModel { /** * Return the length of this text. - * @param {string} s - The given text. + * @param s - The given text. */ - getTextLength(s) { + getTextLength(s: string): number { return s.length * 12; } /** * Create a MemoryModel given a list of JS objects. * - * @param {DrawnEntity[]} objects - the list of objects (including stack-frames) to be drawn. + * @param objects - the list of objects (including stack-frames) to be drawn. * Each object in 'objects' must include the following structure: - * @param {number} objects[*].x - Value for x coordinate of top left corner - * @param {number} objects[*].y - Value for y coordinate of top left corner - * @param {string} objects[*].type - Specifies whether a class, stack frame, or object is being drawn. + * @param objects[*].x - Value for x coordinate of top left corner + * @param objects[*].y - Value for y coordinate of top left corner + * @param objects[*].type - Specifies whether a class, stack frame, or object is being drawn. * To draw a class, input `.class` and to draw a stack frame, input `.frame`. If an * object is being drawn, input the type of the object. - * @param {string} objects[*].name - The name of the class or stack frame to be drawn. Note that this attribute is only + * @param objects[*].name - The name of the class or stack frame to be drawn. Note that this attribute is only * applicable if the object's type is `.class` or `.frame`. If no classes or stack frames * are being drawn, this attribute can be excluded from the input. - * @param {number} objects[*].id - The id value of this object. If we are to draw a StackFrame, then this MUST be 'null'. - * @param {*} objects[*].value - The value of the object. This could be anything, from an empty string to a JS object, + * @param objects[*].id - The id value of this object. If we are to draw a StackFrame, then this MUST be 'null'. + * @param objects[*].value - The value of the object. This could be anything, from an empty string to a JS object, * which would be passed for the purpose of drawing a user-defined class object, a * stackframe, or a dictionary. Note that in such cases where we want to do draw a 'container' * object (an object that contains other objects), we pass a JS object where the keys are the * attributes/variables and the values are the id's of the corresponding objects (not the * objects themselves). - * @param {boolean=} objects[*].show_indexes = false - Applicable only for drawing tuples or lists (when drawSequence + * @param objects[*].show_indexes = false - Applicable only for drawing tuples or lists (when drawSequence * method will be used). * Whether the memory box of the underlying * sequence will include indices (for sequences) or not. This * has a default value of false, and it shall be manually set * only if the object corresponds to a sequence (list or * tuple). - * @param {object} objects[*].style - The style object with which the object will be rendered. Check the + * @param objects[*].style - The style object with which the object will be rendered. Check the * `style.md` and `presets.md` documentation files in the `explanations` directory. * * Preconditions: * - 'objects' is a valid object with the correct properties, as outlined above. */ - drawAll(objects) { - const sizes_arr = []; + drawAll(objects: DrawnEntity[]): Rect[] { + const sizes_arr: Rect[] = []; for (const obj of objects) { if (Array.isArray(obj.style)) { @@ -817,7 +905,7 @@ export class MemoryModel { obj.style = styleSoFar; } - obj.style = { ...obj.style, config: this.roughjs_config }; + obj.style = { ...obj.style, ...this.roughjs_config?.options }; const frame_types = [".frame", ".blank-frame"]; if (frame_types.includes(obj.type) || obj.type === ".class") { diff --git a/memory-viz/src/style.ts b/memory-viz/src/style.ts index 34032faa..64922105 100644 --- a/memory-viz/src/style.ts +++ b/memory-viz/src/style.ts @@ -1,9 +1,11 @@ import merge from "deepmerge"; import { config } from "./config"; -import { DrawnEntity, AttributeStyle, Style } from "./types"; +import { Style } from "./types"; +import type * as CSS from "csstype"; import { MemoryModel } from "./memory_model"; +import { Options } from "roughjs/bin/core"; -const immutable: Array = [ +const immutable: string[] = [ "int", "str", "tuple", @@ -12,41 +14,34 @@ const immutable: Array = [ "float", "date", ]; -const collections: Array = ["list", "set", "tuple", "dict"]; +const collections: string[] = ["list", "set", "tuple", "dict"]; -const primitives: Array = [ - "int", - "str", - "None", - "bool", - "float", - "date", -]; +const primitives: string[] = ["int", "str", "None", "bool", "float", "date"]; // Constants employed to establish presets for styles. -const HIGHLIGHT_TEXT: AttributeStyle = { +const HIGHLIGHT_TEXT: CSS.PropertiesHyphen = { "font-weight": "bolder", "font-size": "22px", }; -const FADE_TEXT: AttributeStyle = { +const FADE_TEXT: CSS.PropertiesHyphen = { /*'font-weight': "normal",*/ "fill-opacity": 0.4, }; -const HIDE_TEXT: AttributeStyle = { "fill-opacity": 0 }; -const HIGHLIGHT_BOX_LINES: AttributeStyle = { roughness: 0.2, strokeWidth: 4 }; -const HIGHLIGHT_BOX: AttributeStyle = { +const HIDE_TEXT: CSS.PropertiesHyphen = { "fill-opacity": 0 }; +const HIGHLIGHT_BOX_LINES: Options = { roughness: 0.2, strokeWidth: 4 }; +const HIGHLIGHT_BOX: Options = { roughness: 0.2, strokeWidth: 4, fill: "yellow", fillStyle: "solid", }; -const FADE_BOX_LINES: AttributeStyle = { roughness: 2.0, strokeWidth: 0.5 }; -const FADE_BOX: AttributeStyle = { +const FADE_BOX_LINES: Options = { roughness: 2.0, strokeWidth: 0.5 }; +const FADE_BOX: Options = { roughness: 2.0, strokeWidth: 0.5, fill: "rgb(247, 247, 247)", fillStyle: "solid", }; -const HIDE_BOX: AttributeStyle = { fill: "white", fillStyle: "solid" }; +const HIDE_BOX: Options = { fill: "white", fillStyle: "solid" }; const presets: Record = { highlight: { diff --git a/memory-viz/src/tests/__snapshots__/cli.spec.tsx.snap b/memory-viz/src/tests/__snapshots__/cli.spec.tsx.snap index 136a529b..0ca8d7af 100644 --- a/memory-viz/src/tests/__snapshots__/cli.spec.tsx.snap +++ b/memory-viz/src/tests/__snapshots__/cli.spec.tsx.snap @@ -141,7 +141,7 @@ exports[`memory-viz cli produces consistent svg when provided filepath and a var path { stroke: rgb(0, 0, 0); } - "David is cool!"id19str" + "David is cool!"id19str" `; exports[`memory-viz cli produces consistent svg when provided filepath and output option(s) 1`] = ` diff --git a/memory-viz/src/tests/draw.spec.tsx b/memory-viz/src/tests/draw.spec.tsx index 9d763461..c907e5bb 100644 --- a/memory-viz/src/tests/draw.spec.tsx +++ b/memory-viz/src/tests/draw.spec.tsx @@ -1,9 +1,10 @@ import exports from "../index"; +import { DrawnEntity } from "../types"; const { MemoryModel, draw } = exports; describe("draw function", () => { it("should produce consistent svg when provided seed", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -27,7 +28,7 @@ describe("draw function", () => { }); it("renders a bool", () => { - const objects: Array = [{ type: "bool", id: 32, value: true }]; + const objects: DrawnEntity[] = [{ type: "bool", id: 32, value: true }]; const m: InstanceType = draw(objects, true, { width: 1300, roughjs_config: { options: { seed: 12345 } }, @@ -37,7 +38,7 @@ describe("draw function", () => { }); it("renders an int", () => { - const objects: Array = [{ type: "int", id: 32, value: 7 }]; + const objects: DrawnEntity[] = [{ type: "int", id: 32, value: 7 }]; const m: InstanceType = draw(objects, true, { width: 1300, roughjs_config: { options: { seed: 12345 } }, @@ -47,7 +48,7 @@ describe("draw function", () => { }); it("renders a float", () => { - const objects: Array = [{ type: "float", id: 32, value: 7.0 }]; + const objects: DrawnEntity[] = [{ type: "float", id: 32, value: 7.0 }]; const m: InstanceType = draw(objects, true, { width: 1300, roughjs_config: { options: { seed: 12345 } }, @@ -57,7 +58,7 @@ describe("draw function", () => { }); it("renders a str", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "str", id: 32, value: "winter" }, ]; const m: InstanceType = draw(objects, true, { @@ -69,7 +70,7 @@ describe("draw function", () => { }); it("renders a set", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "set", id: 32, value: [10, 11, 12] }, ]; const m: InstanceType = draw(objects, true, { @@ -81,7 +82,7 @@ describe("draw function", () => { }); it("renders an empty set", () => { - const objects: Array = [{ type: "set", id: 32, value: [] }]; + const objects: DrawnEntity[] = [{ type: "set", id: 32, value: [] }]; const m: InstanceType = draw(objects, true, { width: 1300, roughjs_config: { options: { seed: 12345 } }, @@ -91,7 +92,7 @@ describe("draw function", () => { }); it("renders a list with indexes showing", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "list", id: 32, @@ -108,7 +109,7 @@ describe("draw function", () => { }); it("renders a list without indexes showing", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "list", id: 32, value: [10, 11, 12] }, ]; const m: InstanceType = draw(objects, true, { @@ -120,7 +121,7 @@ describe("draw function", () => { }); it("renders an empty list", () => { - const objects: Array = [{ type: "list", id: 32, value: [] }]; + const objects: DrawnEntity[] = [{ type: "list", id: 32, value: [] }]; const m: InstanceType = draw(objects, true, { width: 1300, roughjs_config: { options: { seed: 12345 } }, @@ -130,7 +131,7 @@ describe("draw function", () => { }); it("renders a tuple with indexes showing", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "tuple", id: 32, @@ -147,7 +148,7 @@ describe("draw function", () => { }); it("renders a tuple without indexes showing", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "tuple", id: 32, value: [10, 11, 12] }, ]; const m: InstanceType = draw(objects, true, { @@ -159,7 +160,7 @@ describe("draw function", () => { }); it("renders an empty tuple", () => { - const objects: Array = [{ type: "tuple", id: 32, value: [] }]; + const objects: DrawnEntity[] = [{ type: "tuple", id: 32, value: [] }]; const m: InstanceType = draw(objects, true, { width: 1300, roughjs_config: { options: { seed: 12345 } }, @@ -169,7 +170,7 @@ describe("draw function", () => { }); it("renders a dict", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "dict", id: 10, @@ -185,7 +186,7 @@ describe("draw function", () => { }); it("renders an empty dict", () => { - const objects: Array = [{ type: "dict", id: 32, value: {} }]; + const objects: DrawnEntity[] = [{ type: "dict", id: 32, value: {} }]; const m: InstanceType = draw(objects, true, { width: 1300, roughjs_config: { options: { seed: 12345 } }, @@ -195,7 +196,7 @@ describe("draw function", () => { }); it("renders an object with no type and no value", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "None", id: 13, value: "None" }, ]; const m: InstanceType = draw(objects, true, { @@ -207,7 +208,7 @@ describe("draw function", () => { }); it("renders a blank space", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".blank", width: 100, height: 200 }, ]; const m: InstanceType = draw(objects, true, { @@ -219,7 +220,7 @@ describe("draw function", () => { }); it("renders a stack frame and an int", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -243,7 +244,7 @@ describe("draw function", () => { }); it("renders a stack frame using manual layout", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { x: 200, y: 200, @@ -264,7 +265,7 @@ describe("draw function", () => { }); it("renders a bool using manual layout", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { x: 750, y: 250, @@ -281,7 +282,7 @@ describe("draw function", () => { }); it("renders a blank stack frame", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".blank-frame", width: 100, height: 200 }, { type: "list", id: 82, value: [19, 43, 28, 49] }, ]; @@ -294,7 +295,7 @@ describe("draw function", () => { }); it("renders blank spaces in automatic layout", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "int", id: 98, @@ -320,7 +321,7 @@ describe("draw function", () => { }); it("formats non-stack frame objects in automatic layout", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "int", id: 98, @@ -354,7 +355,7 @@ describe("draw function", () => { }); it("formats a mix of stack frame/non-stack frame objects in automatic layout", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -393,7 +394,7 @@ describe("draw function", () => { }); it("renders custom style (without presets)", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "str", id: 19, @@ -417,7 +418,7 @@ describe("draw function", () => { }); it("renders 'highlight' style preset", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -443,7 +444,7 @@ describe("draw function", () => { }); it("renders 'highlight_id' style preset", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -469,7 +470,7 @@ describe("draw function", () => { }); it("renders 'highlight_type' style preset", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -495,7 +496,7 @@ describe("draw function", () => { }); it("renders 'hide' style preset", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -521,7 +522,7 @@ describe("draw function", () => { }); it("renders 'hide_id' style preset", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -547,7 +548,7 @@ describe("draw function", () => { }); it("renders 'hide_container' style preset", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -573,7 +574,7 @@ describe("draw function", () => { }); it("renders 'fade' style preset", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -599,7 +600,7 @@ describe("draw function", () => { }); it("renders 'fade_type' style preset", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -625,7 +626,7 @@ describe("draw function", () => { }); it("renders 'fade_id' style preset", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -651,7 +652,7 @@ describe("draw function", () => { }); it("renders combinations of style presets", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -677,7 +678,7 @@ describe("draw function", () => { }); it("renders diagrams with provided roughjs_config 'fill' option", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "str", id: 42, @@ -693,7 +694,7 @@ describe("draw function", () => { }); it("renders diagrams with provided roughjs_config 'fill' and 'fillStyle' options", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "str", id: 42, @@ -711,7 +712,7 @@ describe("draw function", () => { }); it("renders diagrams with provided roughjs_config 'roughness' option", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "str", id: 42, @@ -727,7 +728,7 @@ describe("draw function", () => { }); it("renders diagrams with provided mix of roughjs_config options", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "str", id: 42, @@ -751,7 +752,7 @@ describe("draw function", () => { }); it("renders range object", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "range", id: 42, @@ -767,7 +768,7 @@ describe("draw function", () => { }); it("logs a warning when provided 'small' width value", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "str", id: 19, @@ -793,7 +794,7 @@ describe("draw function", () => { }); it("renders a diagram with 'small' width value and no stack frames", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: "str", id: 19, @@ -810,7 +811,7 @@ describe("draw function", () => { }); it("renders a diagram with 'small' width value and a mix stack frame/non-stack frame objects", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -850,7 +851,7 @@ describe("draw function", () => { }); it("renders a diagram with large left margins", () => { - const objects: Array = [ + const objects: DrawnEntity[] = [ { type: ".frame", name: "__main__", @@ -889,4 +890,21 @@ describe("draw function", () => { const svg: String = m.serializeSVG(); expect(svg).toMatchSnapshot(); }); + + it("throws an error when object type is not a collection and value is not a primitive", () => { + const objects: DrawnEntity[] = [ + { + type: "invalid collection", + id: 0, + value: [1, 2], + }, + ]; + + const errorMessage = `Invalid type or value: Expected a collection type (dict, set, list, tuple) or a primitive value, but received type "${objects[0].type}" with value "${objects[0].value}".`; + expect(() => + draw(objects, true, { + width: 100, + }) + ).toThrow(errorMessage); + }); }); diff --git a/memory-viz/src/typeguards.ts b/memory-viz/src/typeguards.ts new file mode 100644 index 00000000..d6d3535c --- /dev/null +++ b/memory-viz/src/typeguards.ts @@ -0,0 +1,25 @@ +import { Style } from "./types"; + +export function isArrayOfType(value: any, type: string): value is T[] { + return ( + Array.isArray(value) && + value.every((element) => typeof element === type) + ); +} + +export function isStyle(value: any): value is Style { + const attributes = [ + "text_id", + "text_type", + "text_value", + "box_id", + "box_type", + "box_container", + ]; + return ( + value !== null && + typeof value === "object" && + !Array.isArray(value) && + attributes.some((key) => key in value) + ); +} diff --git a/memory-viz/src/types.ts b/memory-viz/src/types.ts index 5608cb93..fa08bc37 100644 --- a/memory-viz/src/types.ts +++ b/memory-viz/src/types.ts @@ -1,26 +1,89 @@ +import { Config, Options } from "roughjs/bin/core"; +import type * as CSS from "csstype"; + +export type Styles = Style | (string | Style)[]; + export interface DrawnEntity { name?: string; type?: string; x?: number; y?: number; - id: number | string; - value: any; + id?: number | null; + value?: any; show_indexes?: boolean; - style?: Style; + style?: Styles; height?: number; width?: number; rowBreaker?: boolean; } -export interface AttributeStyle { - [propName: string]: string | number; +export interface Style { + text_id?: CSS.PropertiesHyphen; + text_type?: CSS.PropertiesHyphen; + text_value?: CSS.PropertiesHyphen; + box_id?: Options; + box_type?: Options; + box_container?: Options; } -export interface Style { - text_id?: AttributeStyle; - text_type?: AttributeStyle; - text_value?: AttributeStyle; - box_id?: AttributeStyle; - box_type?: AttributeStyle; - box_container?: AttributeStyle; +export interface DisplaySettings { + width: number; + height: number; + sort_by: SortOptions; + style: Styles; + roughjs_config: Config; + padding: number; + top_margin: number; + left_margin: number; + bottom_margin: number; + right_margin: number; +} + +export interface VisualizationConfig { + width: number; + height: number; + rect_style: { + stroke: string; + }; + text_color: string; + value_color: string; + id_color: string; + item_min_width: number; + item_min_height: number; + obj_min_width: number; + obj_min_height: number; + prop_min_width: number; + prop_min_height: number; + obj_x_padding: number; + double_rect_sep: number; + list_index_sep: number; + font_size: number; + browser: boolean; + roughjs_config: Config; +} + +export interface Size { + width: number; + height: number; +} + +export interface Point { + x: number; + y: number; } + +export interface Rect extends Point, Size {} + +export enum SortOptions { + Height, + Id, +} + +export type Primitive = + | string + | number + | boolean + | null + | undefined + | bigint + | symbol; diff --git a/memory-viz/src/user_functions.ts b/memory-viz/src/user_functions.ts index f276a513..639be77b 100644 --- a/memory-viz/src/user_functions.ts +++ b/memory-viz/src/user_functions.ts @@ -1,9 +1,11 @@ import { MemoryModel } from "./memory_model"; import { drawAutomated, getSize } from "./automate"; -import { DrawnEntity } from "./types"; +import { DrawnEntity, DisplaySettings } from "./types"; +import type * as fsType from "fs"; +export * from "./types"; // Dynamic import of Node fs module -let fs; +let fs: typeof fsType | undefined; if (typeof window === "undefined") { fs = require("fs"); } @@ -13,22 +15,26 @@ if (typeof window === "undefined") { * * The format of the array of objects must adhere to the description provided in MemoryModel.drawAll. * - * @param {string | DrawnEntity[]} objects - The array of objects to be drawn: this could be passed as an actual JavaScript + * @param objects - The array of objects to be drawn: this could be passed as an actual JavaScript * array of objects, or as a JSON file containing the object array. This array of objects may also include the * user-defined style configuration. See the demo files and style.md file for details. - * @param {boolean} automation - Whether the coordinates (of the objects on the canvas) should be automatically + * @param automation - Whether the coordinates (of the objects on the canvas) should be automatically * generated or manually inputted. - * @param {Object} configuration - The configuration (display settings) defined by the user. + * @param configuration - The configuration (display settings) defined by the user. * This is also the place to define `sort_by` ("height" or "id") for the object space. * NOTE: In the case that automation == true, then the user must define configuration.width, * as this will be used as the "max canvas width" for the automation process. * If automation == false, then all configuration properties are optional, and the function * will still operate even without defining them. * - * @returns {MemoryModel} the produced canvas + * @returns the produced canvas */ -function draw(objects, automation, configuration) { - let objs; +function draw( + objects: string | DrawnEntity[], + automation: boolean, + configuration: Partial +): MemoryModel { + let objs: DrawnEntity[]; if (typeof objects === "string") { const json_string = fs.readFileSync(objects, "utf-8"); @@ -39,10 +45,10 @@ function draw(objects, automation, configuration) { objs = objects; } - let m; + let m: MemoryModel; if (automation) { - if (!configuration.hasOwnProperty("width")) { + if (configuration.width === undefined) { throw new Error( "Width argument for automated drawing is required." ); diff --git a/package-lock.json b/package-lock.json index 2a183947..58c3ca9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,7 @@ "@types/jest": "^29.5.12", "@types/node": "^20.10.5", "babel-loader": "^9.1.0", + "csstype": "^3.1.3", "husky": "^9.1.5", "jest": "^29.7.0", "tmp": "^0.2.3",