UNPKG

@nasp/icons

Version:

Universal design and icon system for NASP Asset Studio, with NaspScript language support.

407 lines (392 loc) 12.1 kB
'use strict'; 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;