Macro Utility Library
The global hai library provides utilities for working with document content and displaying notifications within macros. It exposes two main utility objects: app and doc.
hai = {
app, // Application utilities (notifications)
doc // Document manipulation utilities
}
Quick Start
Use object destructuring for cleaner code:
const {app, doc} = hai;
// Display a notification
app.notify({
type: "success",
message: "Task completed!",
sticky: true
});
// Update document content
doc.setContent("New content here");
// Find and modify directives
const myDiv = doc.getDirectiveById('my-div');
myDiv.class = 'highlight';
myDiv.innerText = 'Updated text';
Understanding Directives
Directives are template elements in your document with the syntax:
{{tagName attr="value" booleanAttr}}content{{/tagName}}
Self-closing directives (input, textarea, button, etc) don't have closing tags:
{{input type="text" id="name"}}
Examples:
{{div id="container" class="main"}}
Some content here
{{/div}}
{{note remove}}Important note{{/note}}
{{input type="text" placeholder="Enter name"}}
hai.app
Application-level utilities for user interactions.
notify
Displays a notification to the user.
Signature:
notify(notification: {
message: string
type: "info" | "success" | "warning" | "error"
sticky?: boolean
title?: string
}): void
Parameters:
message(string, required) - The main notification messagetype(string, required) - Notification type: "info", "success", "warning", or "error"sticky(boolean, optional) - If true, notification remains until manually removeedtitle(string, optional) - Optional title for the notification
Examples:
// Simple info notification
app.notify({
type: "info",
message: "Processing started..."
});
// Success notification with title
app.notify({
type: "success",
title: "Complete!",
message: "All tasks have been processed",
sticky: false
});
// Sticky warning that requires removeal
app.notify({
type: "warning",
message: "Please review the changes before saving",
sticky: true
});
// Error notification
app.notify({
type: "error",
title: "Error",
message: "Failed to save document"
});
hai.doc
Utilities for manipulating document content and directives. Methods return DirectiveObject instances that provide powerful DOM-like manipulation capabilities.
createDirective
Creates a new directive that can be inserted into the document.
Signature:
createDirective(localName: string): DirectiveObject
Parameters:
localName(string) - The tag name for the directive (e.g., 'div', 'note', 'input')
Returns: A new DirectiveObject instance
Note: Self-closing directives (input, textarea, button, option) are automatically created without closing tags.
Examples:
// Create a div directive
const newDiv = doc.createDirective('div');
newDiv.id = 'my-div';
newDiv.class = 'container';
newDiv.innerText = 'Hello World';
// Insert it into the document
const existingDiv = doc.getDirectiveById('parent');
existingDiv.insertAfter(newDiv);
// Create a self-closing input directive
const input = doc.createDirective('input');
input.type = 'text';
input.placeholder = 'Enter your name';
getContent
Gets the current document content.
Signature:
getContent(): string
Returns: The document content as a string
Example:
const content = doc.getContent();
console.log('Document has', content.length, 'characters');
setContent
Sets the document content, replacing all existing content.
Signature:
setContent(content: string): void
Parameters:
content(string) - The new content for the document
Example:
doc.setContent('{{div}}New document content{{/div}}');
getTitle
Gets the current document title.
Signature:
getTitle(): string
Returns: The document title
Example:
const title = doc.getTitle();
console.log('Document title:', title);
setTitle
Sets the document title.
Signature:
setTitle(title: string): void
Parameters:
title(string) - The new title for the document
Example:
doc.setTitle('My Updated Document');
getDirectiveById
Finds a directive by its id attribute.
Signature:
getDirectiveById(id: string): DirectiveObject | undefined
Parameters:
id(string) - The id to search for
Returns: The matching DirectiveObject or undefined if not found
Example:
const container = doc.getDirectiveById('main-container');
if (container) {
container.class = 'active';
}
getDirectiveByAttVal
Finds the first directive with a specific attribute value.
Signature:
getDirectiveByAttVal(att: string, val: string): DirectiveObject | undefined
Parameters:
att(string) - The attribute name to search forval(string) - The attribute value to match
Returns: The matching DirectiveObject or undefined if not found
Example:
// Find first directive with data-type="important"
const important = doc.getDirectiveByAttVal('data-type', 'important');
if (important) {
console.log('Found:', important.tagName);
}
getDirectivesByAttVal
Finds all directives with a specific attribute value.
Signature:
getDirectivesByAttVal(att: string, val: string): DirectiveObject[]
Parameters:
att(string) - The attribute name to search forval(string) - The attribute value to match
Returns: Array of matching DirectiveObject instances
Example:
// Find all directives with status="pending"
const pending = doc.getDirectivesByAttVal('status', 'pending');
console.log(`Found ${pending.length} pending items`);
pending.forEach(item => {
item.status = 'completed';
});
getDirectivesByClass
Finds all directives with a specific CSS class name.
Signature:
getDirectivesByClass(cls: string): DirectiveObject[]
Parameters:
cls(string) - The class name to search for
Returns: Array of matching DirectiveObject instances
Note: This searches for the class name within space-separated class lists.
Example:
// Find all directives with class "highlight"
const highlighted = doc.getDirectivesByClass('highlight');
// Remove highlight from all
highlighted.forEach(item => {
item.class = item.class.replace('highlight', '').trim();
});
getDirectivesByTagName
Finds all directives with a specific tag name.
Signature:
getDirectivesByTagName(tagName: string): DirectiveObject[]
Parameters:
tagName(string) - The tag name to search for (e.g., 'div', 'note')
Returns: Array of matching DirectiveObject instances
Example:
// Find all note directives
const notes = doc.getDirectivesByTagName('note');
console.log(`Document contains ${notes.length} notes`);
// Add remove attribute to all notes
notes.forEach(note => {
note.remove = true;
});
DirectiveObject
Represents a directive in the document, providing methods to read and manipulate it. Attributes can be accessed and modified directly as object properties.
Properties
tagName
The tag name of the directive (read-only).
Type: string (readonly)
Example:
const dir = doc.getDirectiveById('my-div');
console.log(dir.tagName); // Outputs: "div"
innerText
The text content inside the directive. Returns undefined for self-closing directives.
Type: string | undefined
Examples:
// Reading innerText
const note = doc.getDirectiveById('my-note');
console.log(note.innerText); // Outputs the text content
// Setting innerText
note.innerText = 'Updated note content';
// Self-closing directives
const input = doc.getDirectivesByTagName('input')[0];
console.log(input.innerText); // Outputs: undefined
// Attempting to set innerText on self-closing throws error
// input.innerText = 'text'; // Error: Cannot set innerText on self-closing directive
Dynamic Attribute Access
You can access and modify any directive attribute directly as a property:
Examples:
const dir = doc.getDirectiveById('example');
// Reading attributes
console.log(dir.id); // "example"
console.log(dir.class); // "container main"
console.log(dir.disabled); // true (for boolean attributes)
// Setting attributes
dir.class = 'new-class';
dir.title = 'Hover text';
dir['data-value'] = '123';
// Boolean attributes
dir.disabled = true; // Adds disabled attribute
dir.readonly = true; // Adds readonly attribute
// Removing attributes
delete dir.class; // Removes class attribute
dir.disabled = false; // Removes disabled attribute
Methods
getAttribute
Gets the value of a specified attribute.
Signature:
getAttribute(name: string): string | null
Parameters:
name(string) - The attribute name
Returns: The attribute value, empty string for boolean attributes, or null if not found
Example:
const dir = doc.getDirectiveById('my-div');
const id = dir.getAttribute('id'); // "my-div"
const cls = dir.getAttribute('class'); // "container"
const disabled = dir.getAttribute('disabled'); // "" (empty string for boolean)
const missing = dir.getAttribute('nonexistent'); // null
setAttribute
Sets or removes an attribute.
Signature:
setAttribute(name: string, val: string | boolean): void
Parameters:
name(string) - The attribute nameval(string | boolean) - The value to set. Usetruefor boolean attributes,false/null/undefinedto remove
Example:
const dir = doc.getDirectiveById('my-div');
// Set string attribute
dir.setAttribute('class', 'container');
dir.setAttribute('data-value', '123');
// Set boolean attribute
dir.setAttribute('disabled', true);
// Remove attribute
dir.setAttribute('disabled', false);
dir.setAttribute('class', null);
getDirectiveById
Searches within the current directive's content for a nested directive by id.
Signature:
getDirectiveById(id: string): DirectiveObject | undefined
Parameters:
id(string) - The id to search for
Returns: The matching DirectiveObject or undefined if not found
Example:
const container = doc.getDirectiveById('container');
const nested = container.getDirectiveById('nested-item');
getDirectiveByAttVal
Searches within the current directive's content for the first nested directive with a specific attribute value.
Signature:
getDirectiveByAttVal(att: string, val: string): DirectiveObject | undefined
Parameters:
att(string) - The attribute name to search forval(string) - The attribute value to match
Returns: The matching DirectiveObject or undefined if not found
Example:
const form = doc.getDirectiveById('my-form');
const submitBtn = form.getDirectiveByAttVal('type', 'submit');
getDirectivesByAttVal
Searches within the current directive's content for all nested directives with a specific attribute value.
Signature:
getDirectivesByAttVal(att: string, val: string): DirectiveObject[]
Parameters:
att(string) - The attribute name to search forval(string) - The attribute value to match
Returns: Array of matching DirectiveObject instances
Example:
const container = doc.getDirectiveById('task-list');
const pending = container.getDirectivesByAttVal('status', 'pending');
getDirectivesByClass
Searches within the current directive's content for all nested directives with a specific class name.
Signature:
getDirectivesByClass(cls: string): DirectiveObject[]
Parameters:
cls(string) - The class name to search for
Returns: Array of matching DirectiveObject instances
Example:
const section = doc.getDirectiveById('main-section');
const alerts = section.getDirectivesByClass('alert');
getDirectivesByTagName
Searches within the current directive's content for all nested directives with a specific tag name.
Signature:
getDirectivesByTagName(tagName: string): DirectiveObject[]
Parameters:
tagName(string) - The tag name to search for
Returns: Array of matching DirectiveObject instances
Example:
const form = doc.getDirectiveById('my-form');
const inputs = form.getDirectivesByTagName('input');
inputs.forEach(input => {
input.required = true;
});
insertAfter
Inserts a directive or string immediately after the current directive.
Signature:
insertAfter(dir: DirectiveObject | string): void
Parameters:
dir(DirectiveObject | string) - The directive to insert
Example:
const div = doc.getDirectiveById('my-div');
// Insert a DirectiveObject
const newNote = doc.createDirective('note');
newNote.innerText = 'Added after';
div.insertAfter(newNote);
// Insert a string
div.insertAfter('{{div}}Another div{{/div}}');
insertBefore
Inserts a directive or string immediately before the current directive.
Signature:
insertBefore(dir: DirectiveObject | string): void
Parameters:
dir(DirectiveObject | string) - The directive to insert
Example:
const div = doc.getDirectiveById('my-div');
// Insert a DirectiveObject
const newNote = doc.createDirective('note');
newNote.innerText = 'Added before';
div.insertBefore(newNote);
// Insert a string
div.insertBefore('{{div}}Another div{{/div}}');
remove
Removes the directive from the document.
Signature:
remove(): void
Warning: The DirectiveObject instance becomes invalid after removal.
Example:
const oldDiv = doc.getDirectiveById('obsolete');
if (oldDiv) {
oldDiv.remove();
// oldDiv is now invalid - don't use it further
}
replaceWith
Replaces the current directive with another directive or string.
Signature:
replaceWith(dir: DirectiveObject | string): void
Parameters:
dir(DirectiveObject | string) - The directive to replace with
Example:
const oldDiv = doc.getDirectiveById('old-div');
// Replace with a DirectiveObject
const newDiv = doc.createDirective('div');
newDiv.id = 'new-div';
newDiv.innerText = 'Replaced content';
oldDiv.replaceWith(newDiv);
// Replace with a string
oldDiv.replaceWith('{{note}}This replaces the old div{{/note}}');
toString
Returns the string representation of the directive.
Signature:
toString(): string
Returns: The directive as a string in the format {{tagName attr="value"}}...{{/tagName}}
Example:
const div = doc.getDirectiveById('my-div');
console.log(div.toString());
// Outputs: {{div id="my-div" class="container"}}Content{{/div}}
// Useful for debugging or logging
const allNotes = doc.getDirectivesByTagName('note');
allNotes.forEach(note => {
console.log(note.toString());
});
Advanced Usage
Creating and Inserting Directives
// Create a new note directive
const note = doc.createDirective('note');
note.id = 'important-note';
note.class = 'highlight warning';
note.remove = true;
note.innerText = 'This is an important reminder!';
// Insert it after an existing directive
const section = doc.getDirectiveById('main-section');
section.insertAfter(note);
Working with Nested Directives
// Find a container and work with its children
const taskList = doc.getDirectiveById('task-list');
// Find all completed tasks within the list
const completed = taskList.getDirectivesByAttVal('status', 'done');
console.log(`Completed tasks: ${completed.length}`);
// Archive completed tasks by adding a class
completed.forEach(task => {
task.class = (task.class || '') + ' archived';
});
Manipulating Classes
const element = doc.getDirectiveById('my-element');
// Get current classes
const classes = element.class ? element.class.split(' ') : [];
// Add a class
if (!classes.includes('active')) {
classes.push('active');
element.class = classes.join(' ');
}
// Remove a class
const filtered = classes.filter(c => c !== 'inactive');
element.class = filtered.join(' ');
// Toggle a class
const hasHighlight = classes.includes('highlight');
if (hasHighlight) {
element.class = classes.filter(c => c !== 'highlight').join(' ');
} else {
classes.push('highlight');
element.class = classes.join(' ');
}
Updating Multiple Directives
// Find all input directives and make them required
const inputs = doc.getDirectivesByTagName('input');
inputs.forEach(input => {
input.required = true;
input.class = 'form-control';
});
// Update all notes with a specific status
const pendingNotes = doc.getDirectivesByAttVal('status', 'pending');
pendingNotes.forEach(note => {
note.status = 'reviewed';
note.class = 'reviewed';
});
Building Complex Structures
// Create a form dynamically
const form = doc.createDirective('div');
form.id = 'user-form';
form.class = 'form-container';
// Create and add input fields
const nameInput = doc.createDirective('input');
nameInput.type = 'text';
nameInput.id = 'name';
nameInput.placeholder = 'Enter your name';
const emailInput = doc.createDirective('input');
emailInput.type = 'email';
emailInput.id = 'email';
emailInput.placeholder = 'Enter your email';
// Build the structure using string concatenation
form.innerText = nameInput.toString() + '\n' + emailInput.toString();
// Insert the form into the document
const container = doc.getDirectiveById('main-container');
container.insertAfter(form);
Common Pitfalls
Self-Closing Directives
Problem: Attempting to set innerText on self-closing directives throws an error.
// ❌ This will throw an error
const input = doc.createDirective('input');
input.innerText = 'text'; // Error!
// ✅ Self-closing directives don't have innerText
const input = doc.createDirective('input');
input.placeholder = 'Use attributes instead';
Boolean Attributes
Problem: Confusion about boolean attribute values.
// Boolean attributes are set with true
dir.disabled = true; // ✅ Adds disabled attribute
dir.readonly = true; // ✅ Adds readonly attribute
// Set to false/null/undefined to remove
dir.disabled = false; // ✅ Removes disabled attribute
delete dir.disabled; // ✅ Also removes disabled attribute
// Getting boolean attributes
console.log(dir.disabled); // true if present, undefined if not
console.log(dir.getAttribute('disabled')); // "" if present, null if not
Working with Removed Directives
Problem: Using a DirectiveObject after calling remove().
const div = doc.getDirectiveById('my-div');
div.remove();
// ❌ Don't use div anymore - it's invalid
// div.class = 'something'; // This won't work as expected
Class Attribute Manipulation
Problem: Not handling space-separated class names correctly.
// ❌ This overwrites all classes
dir.class = 'new-class';
// ✅ Append to existing classes
const classes = dir.class ? dir.class.split(' ') : [];
classes.push('new-class');
dir.class = classes.join(' ');
// ✅ Or use string concatenation
dir.class = (dir.class || '') + ' new-class';
Best Practices
-
Check for existence before manipulating:
const element = doc.getDirectiveById('my-id');
if (element) {
element.class = 'active';
} -
Use appropriate search methods:
- Use
getDirectiveById()for unique elements - Use
getDirectivesByTagName()to find all elements of a type - Use
getDirectivesByClass()for elements sharing a class - Use
getDirectivesByAttVal()for custom attributes
- Use
-
Prefer property access for simple operations:
// ✅ Simple and readable
dir.id = 'new-id';
dir.class = 'container';
// Also valid but more verbose
dir.setAttribute('id', 'new-id');
dir.setAttribute('class', 'container'); -
Use
createDirective()for dynamic content:const newDiv = doc.createDirective('div');
newDiv.id = 'dynamic-' + Date.now();
newDiv.innerText = 'Dynamic content'; -
Always handle array results:
const items = doc.getDirectivesByClass('item');
if (items.length > 0) {
items.forEach(item => {
// Process each item
});
}