UNPKG

ef.qt

Version:
748 lines (633 loc) 22.3 kB
'use strict' const camelCase = require('camelcase') const parseEft = require('eft-parser') const crypto = require('crypto') const fs = require('fs-extra') const path = require('path') const STRPROPS = new Set([ 'windowTitle', 'text', 'placeholderText', 'title', 'currentText', 'styleSheet', 'statusTip', 'toolTip', 'whatsThis', 'accessibleName', 'accessibleDescription', 'windowFilePath', 'windowRole' ]) const BOOLPROPS = new Set([ 'checked', 'enabled', 'openExternalLinks', 'acceptDrops', 'autoFillBackground', 'editFocus', 'mouseTracking', 'tabletTracking', 'updatesEnabled', 'disabled', 'hidden', 'visible', 'windowModified', 'documentMode', 'animated', 'dockNestingEnabled', 'unifiedTitleAndToolBarOnMac', 'defaultUp', 'nativeMenuBar', 'separatorsCollapsible', 'tearOffEnabled', 'toolTipsVisible', 'widgetResizable' ]) const FLOATPROPS = new Set([]) const DOUBLEPROPS = new Set([]) const typeDefRegex = /^\((.+?)\)/ const T = { STRING: 'EFVar<QString>', BOOL: 'EFVar<bool>', FLOAT: 'EFVar<float>', DOUBLE: 'EFVar<double>', INT: 'EFVar<int>' } const B = { [T.STRING]: 'const QString&', [T.BOOL]: 'bool', [T.FLOAT]: 'float', [T.DOUBLE]: 'double', [T.INT]: 'int' } const U = { 'string': [T.STRING, B[T.STRING]], 'bool': [T.BOOL, B[T.BOOL]], 'float': [T.FLOAT, B[T.FLOAT]], 'double': [T.DOUBLE, B[T.DOUBLE]], 'int': [T.INT, B[T.INT]] } const getTypeDef = (propName) => { if (STRPROPS.has(propName)) return T.STRING if (BOOLPROPS.has(propName)) return T.BOOL if (FLOATPROPS.has(propName)) return T.FLOAT if (DOUBLEPROPS.has(propName)) return T.DOUBLE return T.INT } const getBaseType = type => B[type] const getUserType = (type) => { if (U[type]) return U[type] return [`EFVar<${type}>`, `const ${type}&`] } const isStringProp = propName => STRPROPS.has(propName) const capitalizeFirstCharacter = lower => lower.charAt(0).toUpperCase() + lower.substring(1) // [varname, usertype, customized] const parseVarName = (varpath) => { const [firstVar, ...vars] = varpath const typeMatch = firstVar.match(typeDefRegex) if (typeMatch) return [[firstVar.replace(typeDefRegex, ''), ...vars].join('.'), getUserType(typeMatch[1]), true] else return [varpath.join('.'), null, false] } const getDynamicArgs = (propName, prop) => { const [strs, ...vars] = prop if (strs === 0) { const [varpath] = vars[0] return `*$data.${parseVarName(varpath)[0]}` } const stringProp = isStringProp(propName) const args = [] for (let i = 0; i < vars.length; i++) { const [varpath] = vars[i] const str = strs[i] if (str !== '') { if (stringProp) args.push(`tr("${str}")`) else args.push(str) } args.push(`*$data.${parseVarName(varpath)[0]}`) } if (strs[vars.length] !== '') { if (stringProp) args.push(`tr("${strs[vars.length]}")`) else args.push(strs[vars.length]) } if (stringProp) return args.join(' + ') else return args.join('') } const walkProps = ({props, innerName, $props, $data}) => { for (let [propName, prop] of Object.entries(props)) { const innerPropName = `__${innerName}_${propName}` if (!$props[innerPropName]) $props[innerPropName] = {innerName, propName} if (Array.isArray(prop)) { // dynamic property const handler = $props[innerPropName].handler || `${innerName}->set${capitalizeFirstCharacter(propName)}(${getDynamicArgs(propName, prop)});` if (!$props[innerPropName].handler) $props[innerPropName].handler = handler // store handlers for variable const [, ...vars] = prop for (let [varpath, defaultVal] of vars) { const [varname, userType, customized] = parseVarName(varpath) if (!$data[varname]) { if (userType) { $data[varname] = { type: userType, handlers: [] } } else { const type = getTypeDef(propName) const baseType = getBaseType(type) $data[varname] = { type: [type, baseType], handlers: [] } } } if (customized) $data[varname].type = userType if (typeof defaultVal !== 'undefined') $data[varname].default = defaultVal $data[varname].handlers.push(handler) } } else { // static property $props[innerPropName].data = prop } } } const walkSignals = ({signals, innerName, type, $methods}) => { for (let signal of signals) { const {l: signalDef, m: handlerName} = signal const [signalName, argStr] = signalDef.split(':') const args = argStr ? argStr.split(',').map(i => i.trim()) : [] const innerMethodName = `__handler_${handlerName}` $methods.push({innerName, innerMethodName, type, signalName, args, handlerName}) } } const walkAst = ({$ast, $parent, $parentLayout, $data, $refs, $methods, $mountingpoints, $props, $widgets, $includes}) => { if (Array.isArray($ast)) { // handle widget const [self, ...children] = $ast const {t: type, r: ref, a: props, e: signals, p: extraProps} = self const innerName = `__widget_${$widgets.length}` $widgets.push({type, parent: $parent, parentLayout: $parentLayout, extraProps: extraProps || {}, innerName}) if (type.startsWith('Q')) $includes.add(`<${type}>`) if (ref) $refs.push({type, innerName, name: ref}) if (props) walkProps({props, innerName, $props, $data}) if (signals) walkSignals({signals, innerName, type, $methods}) const isLayout = type.includes('Layout') for (let i of children) walkAst({ $ast: i, $parent: isLayout ? $parent : innerName, $parentLayout: isLayout ? innerName : null, $data, $refs, $methods, $mountingpoints, $props, $widgets, $includes }) } else { // handle mounting point const {n: name, t: type} = $ast $mountingpoints.push({name, list: !!type}) $widgets.push({name, parent: $parent, parentLayout: $parentLayout, mountpoint: true}) } } const generate$usings = ($usings) => { const strs = [] for (let using of $usings) { strs.push(`using ${using};`) } return strs } const generate$data = ($data) => { // varname: {type: [eftype, basetype], default, handlers} const strs = [] for (let [varname, {type}] of Object.entries($data)) { strs.push(`${type[0]} ${varname};`) } return strs } const generate$refs = ($refs) => { // {type, innerName, *name} const strs = [] for (let {type, name} of $refs) { strs.push(`${type} *${name};`) } return strs } const generate$methods = ($methods, className) => { // {innerName, innerMethodName, type, signalName, args: [type, type], handlerName} const strs = [] const generated = {} for (let {handlerName, args} of $methods) { if (!generated[handlerName]) { generated[handlerName] = true strs.push(`std::function<void(${className}&${args.length ? ['', ...args].join(', ') : ''})> ${handlerName};`) } } return strs } const generate$mountingpoints = ($mountingpoints) => { // {list: bool, parentLayout, name} const strs = [] for (let {list, name} of $mountingpoints) { strs.push(`${list && 'EFListMountingPoint' || 'EFMountingPoint'} ${name};`) } return strs } const generate$widgetsDefinitation = ($widgets) => { // {type, parent, parentLayout, extraProps, innerName} || {mountpoint: bool, name} const strs = [] for (let widget of $widgets) { if (!widget.mountpoint) { const {type, innerName} = widget strs.push(`${type} *${innerName};`) } } return strs } const generate$handlers = ($methods) => { // {innerName, innerMethodName, type, signalName, args: [type, type], handlerName} const strs = [] const generated = {} for (let {innerMethodName, args, handlerName} of $methods) { if (!generated[handlerName]) { generated[handlerName] = true const argArr = args.map((curr, index) => [curr, `__v${index}`]) const wrapperArgs = argArr.map(curr => curr.join(' ')).join(', ') const handlerArgs = ['', ...argArr.map(curr => curr[1])].join(', ') strs.push(`void ${innerMethodName}(${wrapperArgs}) { if ($methods.${handlerName}) $methods.${handlerName}(*this${handlerArgs}); }`) } } return strs } const guseeWidgetClass = (widgetName) => { widgetName = widgetName.toLowerCase() if (widgetName.includes('item')) return 'Item' if (widgetName.includes('layout')) return 'Layout' return 'Widget' } const generate$childInitialization = ({strs, type, widgetClass, previousLayer, previousLayerType, extraProps, innerName}) => { if (!previousLayer) return const [previousWidgetType, previousClass] = previousLayerType let method = 'add' let params = '' // extraProps.params && `${innerName}, ${extraProps.params}` || innerName switch (previousClass) { case 'Item': throw new TypeError(`Item ${type} cannot have children.`) case 'Layout': if (previousWidgetType === 'QGridLayout') { const [,, index, width] = previousLayerType if (!width && !extraProps.position) throw new SyntaxError('QGridLayout must have `width\' attribure ro children of QGridLayout must have `position\' attribute.') previousLayerType[2] += 1 if (extraProps.position) params = `${innerName}, ${extraProps.position}` else { let row = Math.floor(index / width) let col = index - row * width params = `${innerName}, ${row}, ${col}` } } else if (previousWidgetType === 'QFormLayout') { method = 'set' if (extraProps.position) params = `${extraProps.position}, ${innerName}` else { const [,, index, position] = previousLayerType const role = position && 'QFormLayout::ItemRole::LabelRole' || 'QFormLayout::ItemRole::FieldRole' if (!position) previousLayerType[2] += 1 previousLayerType[3] = !previousLayerType[3] params = `${index}, ${role}, ${innerName}` } } else params = innerName break case 'Widget': if (widgetClass === 'Layout') { method = 'set' params = innerName } else return break default: } strs.push(`${previousLayer}->${method}${widgetClass}(${params});`) } const generate$widgetInitialization = ($widgets) => { // {type, parent, parentLayout, extraProps, innerName} || {mountpoint: bool, name} const strs = [] const widgetTypeMap = {} // [type, class, (index, position) || (index, width)] let topInitialized = false for (let widget of $widgets) { if (widget.mountpoint) { const {name, parent} = widget strs.push(`${name}.__set_widget(${parent});`) } else { const {type, parent, parentLayout, extraProps, innerName} = widget const previousLayer = parentLayout || parent const previousLayerType = widgetTypeMap[previousLayer] || [] const widgetClass = guseeWidgetClass(type) const typeInfo = [type, widgetClass] widgetTypeMap[innerName] = typeInfo if (type === 'QFormLayout') { typeInfo[2] = 0 typeInfo[3] = true } else if (type === 'QGridLayout' && extraProps.width) { typeInfo[2] = 0 typeInfo[3] = extraProps.width } if (topInitialized) { if (type.includes('Spacer')) strs.push(`${innerName} = new ${type}(0, 0);`) else if (previousLayerType[1] === 'Layout' && widgetClass === 'Layout') strs.push(`${innerName} = new ${type}();`) else strs.push(`${innerName} = new ${type}(${parent || ''});`) generate$childInitialization({strs, type, widgetClass, previousLayer, previousLayerType, extraProps, innerName}) } else { strs.push(`${innerName} = this;`) topInitialized = true } } } return strs } const generate$refsInitialization = ($refs) => { // {type, innerName, *name} const strs = [] for (let {innerName, name} of $refs) { strs.push(`$refs.${name} = ${innerName};`) } return strs } const generate$valueSubscribers = ($data) => { // varname: {type: [eftype, basetype], default, handlers} const strs = [] for (let [varname, {type, handlers}] of Object.entries($data)) { strs.push(`$data.${varname}.subscribe(std::make_shared<std::function<void(${type[1]})>>( [this](auto _){ ${handlers.join(` `)} } ));`) } return strs } const generate$methodsInitialization = ($methods, className) => { // {innerName, innerMethodName, type, signalName, args: [type, type], handlerName} const strs = [] for (let {innerName, innerMethodName, type, signalName, args} of $methods) { strs.push(`QObject::connect(${innerName}, &${type}::${signalName}, std::bind(&${className}::${innerMethodName}, this${args.length && ', ' || ''}${args.map((_, idx) => `_${idx + 1}`).join(', ')}));`) } return strs } const generate$dataInitialization = ($data) => { // varname: {type, default, handlers} const strs = [] for (let [varname, {type, default: defaultVal}] of Object.entries($data)) { if (typeof defaultVal !== 'undefined') { if (type[0].toLowerCase().includes('string')) strs.push(`$data.${varname} = tr("${defaultVal}");`) else strs.push(`$data.${varname} = ${defaultVal};`) } } return strs } const generate$props = ($props) => { // innerPropName: {innerName, propName, data || handler} const strs = [] for (let [, {innerName, propName, data}] of Object.entries($props)) { if (typeof data !== 'undefined') { if (isStringProp(propName)) strs.push(`${innerName}->set${capitalizeFirstCharacter(propName)}(tr("${data}"));`) else strs.push(`${innerName}->set${capitalizeFirstCharacter(propName)}(${data});`) } } return strs } const generateClass = ({filePath, fileHash, className, nameSpace, $customUsings, $data, $refs, $methods, $mountingpoints, $props, $widgets}) => { const proto = $widgets[0].type return `// source: ${filePath}:${fileHash} namespace ef::ui${nameSpace && `::${nameSpace}` || ''} { // Custom using ${generate$usings($customUsings).join(` `)} class ${className}: public ${proto} { public: // Data variables struct { ${generate$data($data).join(` `)} } $data; // Widget references struct { ${generate$refs($refs).join(` `)} } $refs; // Signal handling methods struct { ${generate$methods($methods, className).join(` `)} } $methods; // Mounting Points ${generate$mountingpoints($mountingpoints).join(` `)} private: // Internal widget names ${generate$widgetsDefinitation($widgets).join(` `)} // Internal signal handlers ${generate$handlers($methods).join(` `)} void __init_widgets() { ${generate$widgetInitialization($widgets).join(` `)} } void __init_refs() { ${generate$refsInitialization($refs).join(` `)} } void __init_value_subscribers() { ${generate$valueSubscribers($data).join(` `)} } void __init_methods() { using namespace std::placeholders; ${generate$methodsInitialization($methods, className).join(` `)} } void __init_data() { ${generate$dataInitialization($data).join(` `)} } void __init_props() { ${generate$props($props).join(` `)} } void __init() { __init_widgets(); __init_refs(); __init_value_subscribers(); __init_methods(); __init_data(); __init_props(); } public: ${className}() { __init(); } template <typename... Args> ${className}(Args... __args) : ${proto}::${proto.split('::').pop()}(std::forward<Args>(__args)...) { __init(); } }; } ` } const checkMetadata = (source) => { const lines = source.split(/\r?\n/) const $customIncludes = new Set() const $customUsings = new Set() let $customNameSpace = null let $customClassName = null for (let line of lines) { const lineContent = line.trim() if (lineContent.startsWith('>')) return {$customIncludes, $customUsings, $customNameSpace, $customClassName} if (lineContent.startsWith(';include ')) $customIncludes.add(line.substring(9, line.length)) else if (lineContent.startsWith(';namespace ')) $customNameSpace = line.substring(11, line.length) else if (lineContent.startsWith(';classname ')) $customClassName = line.substring(11, line.length) else if (lineContent.startsWith(';using ')) $customUsings.add(line.substring(7, line.length)) } return {$customIncludes, $customUsings, $customNameSpace, $customClassName} } const compile = ([filePath, {className, nameSpace, source}]) => { console.log('Processing', filePath, '...') const $data = {} // varname: {type, default, handlers} const $refs = [] // {type, innerName, *name} const $methods = [] // {innerName, innerMethodName, type, signalName, args: [type, type], handlerName} const $mountingpoints = [] // {list: bool, parentLayout, name} const $props = {} // innerPropName: {innerName, propName, data || handler} const $widgets = [] // {type, parent, parentLayout, extraProps, innerName} || {mountpoint: bool, name, parent} const $includes = new Set() const fileHash = crypto .createHash('md5') .update(source) .digest('hex') try { const ast = parseEft(source) const {$customIncludes, $customUsings, $customNameSpace, $customClassName} = checkMetadata(source) if ($customNameSpace !== null) nameSpace = $customNameSpace if ($customClassName !== null) className = $customClassName walkAst({$ast: ast, $parent: null, $parentLayout: null, $data, $refs, $methods, $mountingpoints, $props, $widgets, $includes}) return [filePath, [ generateClass({filePath, fileHash, className, nameSpace, $customUsings, $data, $refs, $methods, $mountingpoints, $props, $widgets}), {className, nameSpace, $includes, $customIncludes} ]] } catch (e) { if (e.message === 'Failed to parse eft template: Template required, but nothing given. at line -1') return ['', {}] throw e } } const generate$includes = ($includes) => { const strs = [] for (let include of $includes) { strs.push(`#include ${include}`) } return strs } const generate$classDefs = $results => [...$results].map(([, [, {className, nameSpace}]]) => { if (nameSpace) return ` namespace ${nameSpace} { class ${className}; }` else return ` class ${className};` }) const generate$results = $results => [...$results].map(([, [result]]) => result) const removeTrailingSpaces = source => source .split('\n') .map(line => line.trimEnd()) .join('\n') const generateSingleFile = ($results) => { const includes = new Set() const customIncludes = new Set() for (let [, [, {$includes, $customIncludes}]] of $results) { for (let include of $includes) includes.add(include) for (let customInclude of $customIncludes) customIncludes.add(customInclude) } return removeTrailingSpaces(` #pragma once #include <QtGui> #include "ef_core.hpp" namespace ef::ui { ${generate$classDefs($results).join('\n')} } // Auto generated includes ${generate$includes(includes).join('\n')} // User defined includes ${generate$includes(customIncludes).join('\n')} using namespace ef::core; ${generate$results($results).join('\n')} `) } const checkNeedsUpdate = ({files, dest, currentVersion}, {verbose, dryrun}, cb) => { const lastSourceHashMap = new Map() fs.readFile(dest, 'utf8', (err, lastGeneratedFile) => { if (err) { if (err.code === 'ENOENT') return cb(null, true) else return console.error(err) } const lines = lastGeneratedFile.split('\n') const lastVersion = lines.shift().split(' ')[4] if (lastVersion !== currentVersion) { if (verbose || dryrun) console.log(`[V] Last generated ef.qt version ${lastVersion} not match with current version ${currentVersion}, regenerate...`) return cb(null, true) } for (let line of lines) { if (line.startsWith('// source: ')) { const content = line.substring(11, line.length) const [filePath, sourceHash] = content.split(':') lastSourceHashMap.set(filePath, sourceHash) } } for (let [filePath, {source}] of files) { const sourceHash = crypto .createHash('md5') .update(source) .digest('hex') if (sourceHash !== lastSourceHashMap.get(filePath)) { if (verbose || dryrun) console.log(`[V] Found hash mismatch in \`${filePath}', regenerate...`) return cb(null, true) } } console.log(`Nothing changed, no need to update \`${dest}'.`) return cb(null, false) }) } const writeOutput = ({$results, dest, currentVersion}, {verbose, dryrun}, cb) => { const outputContent = `// Generated by ef.qt ${currentVersion} on ${(new Date()).toDateString()} ${generateSingleFile($results)}` if (verbose || dryrun) console.log('[V] Writing generated header to:', dest) if (dryrun) { console.log(`Done: Header NOT generated in \`${dest}'. (--dryrun)`) if (cb) return cb(null, {$results, dest, currentVersion}) return } fs.ensureDir(path.dirname(dest), (err) => { if (err) { if (cb) return cb(err) return console.error(err) } fs.outputFile(dest, outputContent, (err) => { if (err) { if (cb) return cb(err) return console.error(err) } console.log(`Done: Header generated in \`${dest}'.`) if (cb) return cb(null, {$results, dest, currentVersion}) }) }) } const generate = ({files, dest}, {verbose, dryrun, watch}, cb) => { fs.readJson(path.resolve(__dirname, 'package.json'), (err, packageInfo) => { if (err) return console.error(err) const {version} = packageInfo const currentVersion = `v${version}` checkNeedsUpdate({files, dest, currentVersion}, {verbose, dryrun}, (err, needsUpdate) => { if (err) { if (cb) return cb(err) return console.error(err) } if (watch || needsUpdate) { try { if (cb) return cb(null, {$results: new Map([...files].map(file => compile(file))), dest, currentVersion, needsUpdate}) } catch (e) { if (cb) return cb(e) return console.error(e) } } }) }) } const getClassNameWithNameSpace = (fileName, dirName) => { const className = camelCase(fileName, {pascalCase: true}) let nameSpace = '' if (dirName !== '.') nameSpace = dirName .replace(/^\.(\\|\/)/, '') .split(path.sep) .map(i => camelCase(i, {pascalCase: true})) .join('::') return [className, nameSpace] } const loadExtraTypeDef = ({extraTypeDef}, {verbose, dryrun}, cb) => { if (verbose || dryrun) console.log('[V] Reading extra param type def:', extraTypeDef) fs.readJson(extraTypeDef, (err, def) => { if (err) { if ((extraTypeDef === '.eftypedef' && err.code !== 'ENOENT') || extraTypeDef !== '.eftypedef') return console.error(err) if (verbose || dryrun) console.log('[V] Default extra param type def read failed, skipped') } else { if (def.STRPROPS) for (let i of def.STRPROPS) STRPROPS.add(i) if (def.BOOLPROPS) for (let i of def.BOOLPROPS) BOOLPROPS.add(i) if (def.FLOATPROPS) for (let i of def.FLOATPROPS) FLOATPROPS.add(i) if (def.DOUBLEPROPS) for (let i of def.DOUBLEPROPS) DOUBLEPROPS.add(i) } return cb() }) } module.exports = {compile, generate, getClassNameWithNameSpace, loadExtraTypeDef, writeOutput}