nihilqui
Version:
Typescript .d.ts generator from GIR for gjs and node-gtk
622 lines (561 loc) • 17.6 kB
text/typescript
/**
* This is where transformations take place for gjs and node-gtk.
* For example a function names should be transformed to lowerCamelCase for node-gtk but should keep their original name for gjs
*/
import {
Transformations,
Environment,
ConstructName,
GenerateConfig,
GirCallableParamElement,
GirEnumElement,
GirBitfieldElement,
} from './types/index.js'
import { lowerCamelCase, upperCamelCase, isFirstCharNumeric, underscores, slugCase, snakeCase } from './utils.js'
import { Logger } from './logger.js'
import { NEW_LINE_REG_EXP } from './constants.js'
import {
WARN_RENAMED_PROPERTY,
WARN_RENAMED_CLASS,
WARN_RENAMED_CONSTANT,
WARN_RENAMED_FIELD,
WARN_RENAMED_FUNCTION,
WARN_RENAMED_ENUM,
WARN_RENAMED_PARAMETER,
} from './messages.js'
export const ARRAY_TYPE_MAP = {
guint8: 'Uint8Array',
// Int8Array would probably be more appropriate for gint8, but Uint8Array is better supported
gint8: 'Uint8Array',
gunichar: 'string',
}
/**
* Used to parse `name` and
* @see https://developer.gnome.org/glib/stable/glib-Basic-Types.html
* @see https://gi.readthedocs.io/en/latest/annotations/giannotations.html
*/
export const PRIMITIVE_TYPE_MAP = {
// Special
none: 'void',
va_list: 'any',
guintptr: 'never', // You can't use pointers in Gjs
// Strings
utf8: 'string',
filename: 'string',
gunichar: 'string',
'gunichar*': 'string',
'char*': 'string',
'gchar*': 'string', // TODO CHECKME
'gchar**': 'string', // TODO CHECKME
'gchar***': 'string', // TODO CHECKME
'const gchar*': 'string', // TODO CHECKME
'const char*': 'string', // TODO CHECKME
// Booleans
gboolean: 'boolean',
'gboolean*': 'boolean',
gpointer: 'any', // This is typically used in callbacks to pass data, so we allow any here.
'gpointer*': 'any',
gconstpointer: 'any',
// Numbers
gchar: 'number',
guchar: 'number',
glong: 'number',
gulong: 'number',
gdouble: 'number',
gssize: 'number',
gsize: 'number',
gshort: 'number',
guint32: 'number',
'guint32*': 'number',
guint: 'number',
'guint*': 'number',
guint8: 'number',
'guint8*': 'number',
'guint8**': 'number',
guint16: 'number',
'guint16*': 'number',
gint16: 'number',
'gint16*': 'number',
guint64: 'number',
'guint64*': 'number',
gfloat: 'number',
'gfloat*': 'number',
gint: 'number',
'gint*': 'number',
gint8: 'number',
'gint8*': 'number',
gint32: 'number',
'gint32*': 'number',
gint64: 'number',
'gint64*': 'number',
gushort: 'number',
'gushort*': 'number',
// Bad numerical types
long: 'number',
double: 'number',
'double*': 'number',
uint32: 'number',
int32: 'number',
uint8: 'number',
int8: 'number',
uint16: 'number',
'int*': 'number',
int: 'number',
// TypeScript types
this: 'this',
never: 'never',
unknown: 'unknown',
any: 'any',
number: 'number',
string: 'string',
boolean: 'boolean',
object: 'object',
void: 'void',
}
// Gjs is permissive for byte-array in parameters but strict for out/return
// See <https://discourse.gnome.org/t/gjs-bytearray-vs-glib-bytes/4900>
export const FULL_TYPE_MAP = (environment: Environment, value: string, out = true): string | undefined => {
let ba: string
let gb: string | undefined
if (environment === 'gjs') {
ba = 'Uint8Array'
if (out === false) {
ba += ' | imports.byteArray.ByteArray'
gb = 'GLib.Bytes | Uint8Array | imports.byteArray.ByteArray'
} else {
gb = undefined // No transformation
}
} else {
// TODO
ba = 'any'
gb = 'any'
}
const fullTypeMap = {
'GObject.Value': 'any',
'GObject.Closure': 'GObject.TClosure',
'GLib.ByteArray': ba,
'GLib.Bytes': gb,
GType: 'GObject.GType',
'GObject.Type': 'GObject.GType',
Type: 'GObject.GType',
'GObject.GType': 'GObject.GType',
}
const result = fullTypeMap[value as keyof typeof fullTypeMap]
return result
}
/** The reserved variable names listed here cannot be resolved simply by quotation marks */
export const RESERVED_QUOTE_VARIABLE_NAMES = ['constructor']
export const RESERVED_VARIABLE_NAMES = [
'in',
'function',
'true',
'false',
'break',
'arguments',
'eval',
'default',
'new',
'extends',
'with',
'var',
'class',
'delete',
'return',
'constructor',
'this', // TODO check if this is used as we would use this in javascript, if so, this is only allowed as the first parameter
]
export const RESERVED_CLASS_NAMES = [
'break',
'boolean',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'number',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'string',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield',
]
export const RESERVED_FUNCTION_NAMES = ['false', 'true', 'break']
/**
* Some classes already have their own `.connect`, `.disconnect` or `.emit` methods,
* so these cannot be overwritten with the Gjs signal methods which have the same name.
*/
export const GOBJECT_SIGNAL_METHOD_NAMES = ['connect', 'connect_after', 'emit', 'disconnect']
export const IGNORE_GIR_TYPE_TS_DOC_TYPES = [
'method',
'enum',
'enum-member',
'bitfield-member',
'property',
'function',
'static-function',
'constant',
]
export const RESERVED_NAMESPACE_NAMES = {}
export class Transformation {
/**
* Rules for the name conventions
* For node-gtk naming conventions see https://github.com/romgrk/node-gtk#naming-conventions
* For gjs see https://gjs-docs.gnome.org/ and https://wiki.gnome.org/Attic/Gjs
*/
private transformations: Transformations = {
functionName: {
node: {
transformation: 'lowerCamelCase',
},
gjs: {
transformation: 'original',
},
},
enumName: {
node: {
transformation: 'original',
},
gjs: {
transformation: 'original',
},
},
enumMember: {
node: {
transformation: 'upperCase',
},
gjs: {
transformation: 'upperCase',
},
},
signalName: {
node: {
transformation: 'original',
},
gjs: {
transformation: 'original',
},
},
// GJS always re-writes - to _ (I think?)
propertyName: {
node: {
transformation: 'lowerCamelCase',
},
gjs: {
transformation: 'underscores',
},
},
parameterName: {
node: {
transformation: 'lowerCamelCase',
},
gjs: {
transformation: 'underscores',
},
},
fieldName: {
node: {
transformation: 'lowerCamelCase',
},
gjs: {
transformation: 'underscores',
},
},
constantName: {
node: {
transformation: 'original',
},
gjs: {
transformation: 'original',
},
},
importNamespaceName: {
node: {
transformation: 'upperCamelCase',
},
gjs: {
transformation: 'upperCamelCase',
},
},
signalInterfaceName: {
node: {
transformation: 'upperCamelCase',
},
gjs: {
transformation: 'upperCamelCase',
},
},
importName: {
node: {
transformation: 'lowerCase',
},
gjs: {
transformation: 'lowerCase',
},
},
}
private log: Logger
constructor(private readonly config: GenerateConfig, logName = 'Transformation') {
this.log = new Logger(config.environment, config.verbose, logName)
}
public transformSignalInterfaceName(name: string): string {
name = this.transform('signalInterfaceName', name)
return name
}
public transformModuleNamespaceName(name: string): string {
name = this.transformNumericName(name)
name = this.transform('importNamespaceName', name)
if (RESERVED_NAMESPACE_NAMES[name]) {
name = `${name}_`
}
return name
}
public transformClassName(name: string): string {
const originalName = `${name}`
name = this.transformNumericName(name)
if (RESERVED_CLASS_NAMES.includes(name)) {
name = `${name}_`
}
if (originalName !== name) {
this.log.warn(WARN_RENAMED_CLASS(originalName, name))
}
return name
}
/**
* // E.g. the NetworkManager-1.0 has enum names starting with 80211
* @param girEnum The enum
* @returns The transformed name
*/
public transformEnumName(girEnum: GirEnumElement | GirBitfieldElement): string {
let name = girEnum.$.name
name = this.transform('enumName', name)
const originalName = `${name}`
// For an example enum starting with a number see https://gjs-docs.gnome.org/nm10~1.20.8/nm.80211mode
name = this.transformNumericName(name)
if (RESERVED_CLASS_NAMES.includes(name)) {
name = `${name}_`
}
if (originalName !== name) {
this.log.warn(WARN_RENAMED_ENUM(originalName, name))
}
return name
}
public transformEnumMember(memberName: string): string {
memberName = this.transform('enumMember', memberName)
memberName = this.transformNumericName(memberName)
return memberName
}
public transformFunctionName(name: string, isVirtual: boolean): string {
name = this.transform('functionName', name)
// node-gtk has no `vfunc_` prefix for virtual methods
if (isVirtual && this.config.environment === 'gjs') {
name = 'vfunc_' + name
}
const originalName = `${name}`
name = this.transformNumericName(name)
if (RESERVED_FUNCTION_NAMES.includes(name)) {
name = `${name}_TODO`
}
if (originalName !== name) {
this.log.warn(WARN_RENAMED_FUNCTION(originalName, name))
}
return name
}
public transformReservedVariableNames(name: string, allowQuotes: boolean) {
if (RESERVED_VARIABLE_NAMES.includes(name)) {
if (allowQuotes && !RESERVED_QUOTE_VARIABLE_NAMES.includes(name)) {
name = `"${name}"`
} else {
name = `${name}_`
}
}
return name
}
/**
* E.g. GstVideo-1.0 has a class `VideoScaler` with a method called `2d`
* or NetworkManager-1.0 has methods starting with `80211`
*/
public transformPropertyName(name: string, allowQuotes: boolean): string {
name = this.transform('propertyName', name)
const originalName = `${name}`
name = this.transformReservedVariableNames(name, allowQuotes)
name = this.transformNumericName(name, allowQuotes)
if (originalName !== name) {
this.log.warn(WARN_RENAMED_PROPERTY(originalName, name))
}
return name
}
public transformConstantName(name: string, allowQuotes: boolean): string {
name = this.transform('constantName', name)
const originalName = `${name}`
name = this.transformReservedVariableNames(name, allowQuotes)
name = this.transformNumericName(name, allowQuotes)
if (originalName !== name) {
this.log.warn(WARN_RENAMED_CONSTANT(originalName, name))
}
return name
}
public transformFieldName(name: string, allowQuotes: boolean): string {
name = this.transform('fieldName', name)
const originalName = `${name}`
name = this.transformReservedVariableNames(name, allowQuotes)
name = this.transformNumericName(name, allowQuotes)
if (originalName !== name) {
this.log.warn(WARN_RENAMED_FIELD(originalName, name))
}
return name
}
public transformParameterName(param: GirCallableParamElement, allowQuotes: boolean): string {
let name = param.$.name || '-'
// Such a variable name exists in `GConf-2.0.d.ts` class `Engine` method `change_set_from_current`
if (name === '...') {
return '...args'
}
name = this.transform('parameterName', name)
const originalName = `${name}`
name = this.transformReservedVariableNames(name, allowQuotes)
name = this.transformNumericName(name, allowQuotes)
if (originalName !== name) {
this.log.warn(WARN_RENAMED_PARAMETER(originalName, name))
}
return name
}
/**
* Fixes type names, e.g. Return types or enum definitions can not start with numbers
* @param typeName
*/
public transformTypeName(name: string): string {
name = this.transformNumericName(name)
return name
}
public transform(construct: ConstructName, transformMe: string): string {
const transformations = this.transformations[construct][this.config.environment].transformation
switch (transformations) {
case 'lowerCamelCase':
return lowerCamelCase(transformMe)
case 'upperCamelCase':
return upperCamelCase(transformMe)
case 'slugCase':
return slugCase(transformMe)
case 'snakeCase':
return snakeCase(transformMe)
case 'upperCase':
return transformMe.toUpperCase()
case 'lowerCase':
return transformMe.toLowerCase()
case 'underscores':
return underscores(transformMe)
case 'original':
default:
return transformMe
}
}
/**
* In JavaScript there can be no variables, methods, class names or enum names that start with a number.
* This method converts such names.
* TODO: Prepends an `@` to numeric starting names, how does gjs and node-gtk do that?
* @param name
* @param allowQuotes
*/
private transformNumericName(name: string, allowQuotes = false): string {
if (isFirstCharNumeric(name)) {
if (allowQuotes) name = `"${name}"`
else name = `TODO_${name}`
}
return name
}
/**
* Replaces "@any_property" with "`any_property`"
* @param description E.g. "Creates a binding between @source_property on @source and @target_property on @target."
* @returns E.g. "Creates a binding between `source_property` on `source` and `target_property` on `target`."
*/
private transformGirDocHighlights(description: string) {
const highlights = description.match(/@[A-Za-z_-]*[^\s.]/gm)
if (highlights) {
for (const highlight of highlights) {
description = description.replace(highlight, `\`${highlight.slice(1)}\``)
}
}
return description
}
/**
* Replaces:
* |[<!-- language="C" -->
* g_object_notify_by_pspec (self, properties[PROP_FOO]);
* ]|
*
* with
* ```c
* g_object_notify_by_pspec (self, properties[PROP_FOO]);
* ```
*
* @param description
*/
private transformGirDocCodeBlocks(description: string) {
description = description
.replaceAll(/\|\[<!-- language="C" -->/gm, '\n```c') // C-Code
.replaceAll(/\|\[/gm, '\n```') // Other code
.replaceAll(/\]\|/gm, '```\n') // End of code
return description
}
/**
* Transforms the documentation text to markdown
* TODO: Transform references to link tags: https://tsdoc.org/pages/tags/link/
* @param text
* @returns
*/
public transformGirDocText(text: string) {
text = this.transformGirDocHighlights(text)
text = this.transformGirDocCodeBlocks(text)
return text
}
/**
* Transforms the tsDoc tag <text> like `@param name <text>`
* @param text
* @returns
*/
public transformGirDocTagText(text: string) {
// return this.transformGirDocHighlights(text.replace(NEW_LINE_REG_EXP, ' '))
return text.replace(NEW_LINE_REG_EXP, ' ')
}
public transformImportName(packageName: string): string {
let importName = this.transform('importName', packageName)
if (this.config.environment === 'node' && !importName.startsWith('node-')) {
importName = 'node-' + importName
}
return importName
}
}