@nasp/icons
Version:
Universal design and icon system for NASP Asset Studio, with NaspScript language support.
407 lines (392 loc) • 12.1 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
// NASPIcon: Universal Icon Loader for NASP Design Kit (modern API)
// Exports for use in Node, ESM, and browser environments
let _icons = {};
let _initialized = false;
function _ensureInit() {
if (!_initialized) throw new Error('NASPIcon: Not initialized. Call NASPIcon.init() first or use NASPIcon.autoInit().');
}
const NASPIcon = {
async init(path = 'icons.json') {
if (_initialized) return;
try {
const response = await fetch(path);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
// Flatten the icon data for easy lookup
for (const categoryKey in data) {
if (data[categoryKey].icons) {
for (const iconKey in data[categoryKey].icons) {
_icons[iconKey] = data[categoryKey].icons[iconKey].svg;
}
}
}
_initialized = true;
} catch (error) {
console.error('NASPIcon: Failed to load icon data.', error);
throw error;
}
},
async autoInit(path = 'icons.json') {
if (typeof window !== 'undefined') {
await this.init(path);
}
},
get(name, props = {}) {
_ensureInit();
let svgString = _icons[name];
if (!svgString) {
console.warn(`NASPIcon: Icon "${name}" not found.`);
return `<svg viewBox="0 0 24 24" width="${props.size || 24}" height="${props.size || 24}"><path d="M12 2c-5.523 0-10 4.477-10 10s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm-1.707 12.707l-1.414-1.414L10.586 12 8.879 10.293l1.414-1.414L12 10.586l1.707-1.707 1.414 1.414L13.414 12l1.707 1.707-1.414 1.414L12 13.414l-1.707 1.707z" fill="red"/></svg>`;
}
// Use a temporary DOM element to easily manipulate attributes
const tempDiv = typeof document !== 'undefined' ? document.createElement('div') : null;
if (!tempDiv) return svgString; // Node.js fallback: just return SVG string
tempDiv.innerHTML = svgString;
const svgElement = tempDiv.firstChild;
if (props.size) {
svgElement.setAttribute('width', props.size);
svgElement.setAttribute('height', props.size);
}
if (props.color) {
svgElement.setAttribute('stroke', props.color);
}
if (props.class) {
svgElement.setAttribute('class', props.class);
}
svgElement.classList.add('nasp-icon');
return svgElement.outerHTML;
},
element(name, props = {}) {
const html = this.get(name, props);
if (typeof document === 'undefined') return null;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
return tempDiv.firstChild;
},
html(name, props = {}) {
return this.get(name, props);
},
appendTo(name, props = {}, parent = document.body) {
const el = this.element(name, props);
if (el && parent && typeof parent.appendChild === 'function') {
parent.appendChild(el);
}
return el;
},
react(name, props = {}) {
// For React: returns a JSX element
if (typeof React === 'undefined') {
throw new Error('NASPIcon.react requires React to be in scope.');
}
const html = this.get(name, props);
return React.createElement('span', {
dangerouslySetInnerHTML: {
__html: html
}
});
},
getIconSVG(name) {
_ensureInit();
return _icons[name] || null;
},
createIcon(name, props = {}) {
return this.get(name, props);
},
async parse() {
if (!_initialized) await this.init();
if (typeof document === 'undefined') return;
const iconTags = document.querySelectorAll('nasp-icon');
iconTags.forEach(tag => {
const name = tag.getAttribute('name');
if (!name) return;
const props = {
size: tag.getAttribute('size'),
color: tag.getAttribute('color'),
class: tag.getAttribute('class')
};
const finalSVG = this.get(name, props);
if (finalSVG) {
tag.outerHTML = finalSVG;
}
});
},
listIcons() {
_ensureInit();
return Object.keys(_icons);
},
hasIcon(name) {
_ensureInit();
return !!_icons[name];
}
};
// Color utilities for NASP Design Kit
function isHexColor(str) {
return /^#([A-Fa-f0-9]{3}){1,2}$/.test(str);
}
function parseColor(str) {
// Accepts hex or color names
if (isHexColor(str)) return str;
// Add more color name support as needed
const named = {
red: '#FF0000',
green: '#00FF00',
blue: '#0000FF',
black: '#000',
white: '#FFF',
yellow: '#FFD700',
purple: '#800080',
orange: '#FFA500',
gray: '#888',
grey: '#888',
pink: '#FFC0CB',
cyan: '#00FFFF',
magenta: '#FF00FF'
};
return named[str.toLowerCase()] || str;
}
// String utilities for NASP Design Kit
function toPascalCase(str) {
return str.replace(/(^|_|-|\s)+(\w)/g, (_, __, c) => c ? c.toUpperCase() : '');
}
// DSL Extensions for NaspScript
const commands = {};
function registerCommand(name, handler) {
commands[name] = handler;
}
function getCommands() {
return {
...commands
};
}
// Example: register a 'spacer' command
registerCommand('spacer', (args, ctx) => {
const size = args.size || 16;
return `<div style="width:${size}px;height:${size}px;display:inline-block;"></div>`;
});
// NaspScript: Improved DSL for NASP Design Kit UI/icon layouts
// Usage: NaspScript.parse(script, { renderIcon })
const VAR_RE = /^let\s+(\w+)\s*=\s*([#\w\d]+)$/;
// Add: repeat { ... } and if { ... } blocks
// Usage: repeat 3 { ... } or if varName { ... }
const REPEAT_RE = /^repeat\s+(\d+)\s*\{/;
const IF_RE = /^if\s+(\w+)\s*\{/;
function parseProps(str, vars, diagnostics, lineNum) {
const props = {};
const sizeMatch = str.match(/size\s+(\d+)/);
if (sizeMatch) props.size = Number(sizeMatch[1]);
const colorMatch = str.match(/color\s+([#a-zA-Z0-9]+)/);
if (colorMatch) props.color = parseColor(vars[colorMatch[1]] || colorMatch[1]);
const classMatch = str.match(/class\s+([\w-]+)/);
if (classMatch) props.class = classMatch[1];
return props;
}
function collectDiagnostics(diagnostics, {
code,
message,
severity,
line,
col
}) {
diagnostics.push({
code,
message,
severity,
line,
col
});
}
const NaspScript = {
/**
* Parses a NaspScript string and returns HTML.
* Supports: icon, row, column, group, repeat, if, variables, comments, custom commands.
* Returns { html, diagnostics } if parseWithDiagnostics is used.
*/
parse(script, {
renderIcon
}) {
return this.parseWithDiagnostics(script, {
renderIcon
}).html;
},
parseWithDiagnostics(script, {
renderIcon
}) {
const lines = script.split(/\r?\n/);
let html = '';
let stack = [];
let vars = {};
let current = {
type: 'root',
content: ''
};
const customCommands = getCommands();
const diagnostics = [];
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (!line) continue;
if (line.startsWith('//')) continue; // comment
if (VAR_RE.test(line)) {
const [, key, value] = line.match(VAR_RE);
vars[key] = value;
continue;
}
if (/^(row|column|group)\s*\{/.test(line)) {
stack.push(current);
current = {
type: RegExp.$1,
content: ''
};
continue;
}
// repeat N { ... }
let repeatMatch = line.match(REPEAT_RE);
if (repeatMatch) {
stack.push(current);
current = {
type: 'repeat',
count: Number(repeatMatch[1]),
content: ''
};
continue;
}
// if varName { ... }
let ifMatch = line.match(IF_RE);
if (ifMatch) {
stack.push(current);
current = {
type: 'if',
varName: ifMatch[1],
content: ''
};
continue;
}
if (line === '}') {
const parent = stack.pop();
if (!parent) {
collectDiagnostics(diagnostics, {
code: 'E001',
message: 'Unmatched closing brace',
severity: 'error',
line: i + 1,
col: 1
});
continue;
}
if (current.type === 'repeat') {
for (let r = 0; r < current.count; r++) {
parent.content += current.content;
}
} else if (current.type === 'if') {
if (vars[current.varName]) {
parent.content += current.content;
}
} else {
let tag = 'div';
let cls = '';
if (current.type === 'row') {
cls = 'nasp-row';
}
if (current.type === 'column') {
cls = 'nasp-column';
}
if (current.type === 'group') {
cls = 'nasp-group';
}
parent.content += `<${tag} class='${cls}'>${current.content}</${tag}>`;
}
current = parent;
continue;
}
// icon line: icon Name [props]
const iconMatch = line.match(/^icon\s+(["']?)([\w-]+)\1(.*)$/);
if (iconMatch) {
const name = vars[iconMatch[2]] || toPascalCase(iconMatch[2]);
if (!NASPIcon.hasIcon(name)) {
collectDiagnostics(diagnostics, {
code: 'W001',
message: `Unknown icon: ${name}`,
severity: 'warning',
line: i + 1,
col: 1
});
}
const props = parseProps(iconMatch[3], vars);
const iconHtml = renderIcon ? renderIcon(name, props) : `<span>${name}</span>`;
current.content += iconHtml;
continue;
}
// custom command: e.g. spacer size 32
const [cmd, ...argsArr] = line.split(/\s+/);
if (customCommands[cmd]) {
// parse args as key-value pairs
const args = {};
for (let j = 0; j < argsArr.length; j += 2) {
const k = argsArr[j];
const v = argsArr[j + 1];
if (k && v) args[k] = v;
}
try {
current.content += customCommands[cmd](args, {
vars,
renderIcon
});
} catch (e) {
collectDiagnostics(diagnostics, {
code: 'E002',
message: `Error in command '${cmd}': ${e.message}`,
severity: 'error',
line: i + 1,
col: 1
});
}
continue;
}
// Unknown line: warning
collectDiagnostics(diagnostics, {
code: 'W002',
message: `Unknown or invalid syntax: ${line}`,
severity: 'warning',
line: i + 1,
col: 1
});
}
if (stack.length > 0) {
collectDiagnostics(diagnostics, {
code: 'E003',
message: 'Unclosed block(s) at end of script',
severity: 'error',
line: lines.length,
col: 1
});
}
html += current.content;
return {
html,
diagnostics
};
},
/**
* Suggests completions for a partial word and context (icons, commands, variables).
*/
suggest(partial, {
icons = [],
commands = [],
variables = []
} = {}) {
const all = [...icons, ...commands, ...variables];
return all.filter(x => x.toLowerCase().startsWith(partial.toLowerCase()));
},
/**
* Highlights NaspScript code as HTML with spans for keywords, variables, numbers, etc.
*/
highlight(script) {
return script.replace(/(\blet\b|\bicon\b|\brow\b|\bcolumn\b|\bgroup\b|\brepeat\b|\bif\b)/g, '<span class="dsl-keyword">$1</span>').replace(/(\d+)/g, '<span class="dsl-number">$1</span>').replace(/#([A-Fa-f0-9]{3,6})/g, '<span class="dsl-color">#$1</span>').replace(/"([^"]*)"/g, '<span class="dsl-string">"$1"</span>').replace(/'([^']*)'/g, '<span class="dsl-string">\'$1\'</span>').replace(/(\/[\/].*)/g, '<span class="dsl-comment">$1</span>');
}
};
if (typeof module !== 'undefined') module.exports = NaspScript;
if (typeof window !== 'undefined') window.NaspScript = NaspScript;
exports.NASPIcon = NASPIcon;
exports.NaspScript = NaspScript;
exports.default = NaspScript;