502 lines
23 KiB
Swift
502 lines
23 KiB
Swift
import Foundation
|
|
#if canImport(FoundationXML)
|
|
import FoundationXML
|
|
#endif
|
|
|
|
/// Errors that can occur during GIR XML parsing.
|
|
public enum GIRParserError: Error {
|
|
/// The XML document is malformed or could not be parsed.
|
|
/// - Parameter description: A human-readable error description.
|
|
case invalidXML(String)
|
|
/// An unexpected XML element was encountered.
|
|
/// - Parameter description: Details about the unexpected element.
|
|
case unexpectedElement(String)
|
|
/// A required attribute is missing from an XML element.
|
|
/// - Parameter description: Description of the missing attribute.
|
|
case missingAttribute(String)
|
|
}
|
|
|
|
/// Parses GIR XML files into the intermediate representation model.
|
|
///
|
|
/// Uses Foundation's `XMLParser` in a SAX-style delegate pattern. The parser
|
|
/// handles all GIR element types including classes, interfaces, records,
|
|
/// enumerations, bitfields, callbacks, methods, properties, signals, functions,
|
|
/// constants, and aliases.
|
|
public struct GIRParser {
|
|
/// Creates a new GIR parser.
|
|
public init() {}
|
|
|
|
/// Parses a GIR XML document from a string.
|
|
/// - Parameter xmlString: The XML content to parse.
|
|
/// - Returns: A `Repository` containing all parsed namespaces and types.
|
|
/// - Throws: `GIRParserError` if the XML cannot be encoded as UTF-8 or parsing fails.
|
|
public func parse(xmlString: String) throws -> Repository {
|
|
guard let data = xmlString.data(using: .utf8) else {
|
|
throw GIRParserError.invalidXML("Failed to encode XML string as UTF-8")
|
|
}
|
|
return try parse(data: data)
|
|
}
|
|
|
|
/// Parses a GIR XML document from a file URL.
|
|
/// - Parameter fileURL: The URL of the .gir file to parse.
|
|
/// - Returns: A `Repository` containing all parsed namespaces and types.
|
|
/// - Throws: `GIRParserError` if the file cannot be read or parsing fails.
|
|
public func parse(fileURL: URL) throws -> Repository {
|
|
let data = try Data(contentsOf: fileURL)
|
|
return try parse(data: data)
|
|
}
|
|
|
|
/// Parses raw XML data using a SAX-style delegate.
|
|
/// - Parameter data: The XML data to parse.
|
|
/// - Returns: A `Repository` with the parsed content.
|
|
/// - Throws: `GIRParserError` if parsing fails.
|
|
private func parse(data: Data) throws -> Repository {
|
|
let delegate = GIRXMLDelegate()
|
|
let parser = XMLParser(data: data)
|
|
parser.delegate = delegate
|
|
if parser.parse(), delegate.parseError == nil {
|
|
return delegate.repository
|
|
}
|
|
throw delegate.parseError ?? GIRParserError.invalidXML(parser.parserError?.localizedDescription ?? "unknown error")
|
|
}
|
|
}
|
|
|
|
// MARK: - XMLParser Delegate
|
|
|
|
/// SAX-style delegate for `XMLParser` that builds a `Repository` from GIR XML.
|
|
///
|
|
/// Tracks a stack of currently-open GIR elements via `current*` properties.
|
|
/// On `didStartElement` it creates the corresponding model object and populates
|
|
/// attributes. On `didEndElement` it appends the completed object to its parent.
|
|
/// The final `repository` property contains the fully parsed GIR document.
|
|
final class GIRXMLDelegate: NSObject, XMLParserDelegate {
|
|
/// The repository being populated during parsing.
|
|
var repository = Repository()
|
|
/// Set to the first error encountered, or `nil` if parsing succeeds.
|
|
var parseError: GIRParserError?
|
|
|
|
var currentNamespace: Namespace?
|
|
var currentClass: Class?
|
|
var currentInterface: Interface?
|
|
var currentRecord: Record?
|
|
var currentEnum: Enumeration?
|
|
var currentBitfield: Bitfield?
|
|
var currentMethod: Method?
|
|
var currentConstructor: Constructor?
|
|
var currentSignal: Signal?
|
|
var currentProperty: Property?
|
|
var currentFunction: GlobalFunction?
|
|
var currentParameter: Parameter?
|
|
var currentCallback: Callback?
|
|
var currentConstant: Constant?
|
|
var currentAlias: Alias?
|
|
var currentReturnType: GIRType?
|
|
var currentText: String = ""
|
|
/// Counter for nested untracked elements that contain child elements.
|
|
/// When > 0, `<doc>` elements belong to untracked parents and should be discarded.
|
|
var untrackedDepth: Int = 0
|
|
|
|
/// Called when the XML parser encounters an opening element tag.
|
|
///
|
|
/// Creates the corresponding model object for the element and populates it
|
|
/// from XML attributes. Maintains a stack of `current*` properties so that
|
|
/// nested elements can attach themselves to their parent on close.
|
|
/// - Parameters:
|
|
/// - parser: The XML parser.
|
|
/// - elementName: The name of the XML element.
|
|
/// - namespaceURI: The namespace URI of the element.
|
|
/// - qualifiedName: The qualified name of the element.
|
|
/// - attributeDict: The element's attributes.
|
|
func parser(_ parser: XMLParser, didStartElement elementName: String,
|
|
namespaceURI: String?, qualifiedName: String?,
|
|
attributes attributeDict: [String: String] = [:]) {
|
|
switch elementName {
|
|
case "c:include":
|
|
guard let name = attributeDict["name"] else { return }
|
|
repository.cHeaderPath = name
|
|
|
|
case "package":
|
|
guard let name = attributeDict["name"] else { return }
|
|
repository.packageName = name
|
|
|
|
case "include":
|
|
guard let name = attributeDict["name"], let version = attributeDict["version"] else { return }
|
|
// GIR dependency includes appear before the <namespace> element
|
|
// Derive link name: lowercase(library) + "-" + major version (e.g. "Gdk-4.0" → "gdk-4")
|
|
let majorVersion = version.split(separator: ".").first.map(String.init) ?? version
|
|
let linkName = "\(name.lowercased())-\(majorVersion)"
|
|
repository.includedLibraryLinks.append(linkName)
|
|
repository.includedPackages.append(IncludeEntry(name: name, version: version))
|
|
|
|
case "namespace":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let version = requireAttribute("version", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let sharedLibrary = attributeDict["shared-library"] ?? ""
|
|
let cIdentifierPrefix = attributeDict["c:identifier-prefixes"] ?? ""
|
|
currentNamespace = Namespace(name: name, version: version, cSharedLibrary: sharedLibrary, cIdentifierPrefix: cIdentifierPrefix)
|
|
|
|
case "class":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let cType = attributeDict["c:type"] ?? ""
|
|
let parent = attributeDict["parent"]
|
|
let isAbstract = attributeDict["abstract"] == "1"
|
|
currentClass = Class(name: name, cType: cType, parent: parent, isAbstract: isAbstract)
|
|
|
|
case "interface":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let cType = attributeDict["c:type"] ?? ""
|
|
let prereqs = attributeDict["prerequisite"]?.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } ?? []
|
|
currentInterface = Interface(name: name, cType: cType, prereqs: prereqs)
|
|
|
|
case "record":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let cType = attributeDict["c:type"] ?? ""
|
|
currentRecord = Record(name: name, cType: cType, isOpaque: attributeDict["opaque"] == "1",
|
|
isDisguised: attributeDict["disguised"] == "1")
|
|
|
|
case "enumeration":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let cType = attributeDict["c:type"] ?? ""
|
|
currentEnum = Enumeration(name: name, cType: cType)
|
|
|
|
case "bitfield":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let cType = attributeDict["c:type"] ?? ""
|
|
currentBitfield = Bitfield(name: name, cType: cType)
|
|
|
|
case "member":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let value = requireAttribute("value", from: attributeDict, for: elementName, parser: parser),
|
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let member = EnumMember(name: name, value: value, cIdentifier: cid)
|
|
currentEnum?.members.append(member)
|
|
currentBitfield?.members.append(member)
|
|
|
|
case "callback":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let cType = attributeDict["c:type"] ?? ""
|
|
currentCallback = Callback(name: name, cType: cType)
|
|
|
|
case "constructor":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
currentConstructor = Constructor(name: name, cIdentifier: cid)
|
|
|
|
case "method":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
currentMethod = Method(name: name, cIdentifier: cid)
|
|
|
|
case "function":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
currentFunction = GlobalFunction(name: name, cIdentifier: cid)
|
|
|
|
case "signal":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
currentSignal = Signal(name: name, isDetailed: attributeDict["detailed"] == "1")
|
|
|
|
case "glib:signal":
|
|
guard let name = attributeDict["name"] ?? attributeDict["glib:name"] else { return }
|
|
currentSignal = Signal(name: name, isDetailed: attributeDict["detailed"] == "1")
|
|
|
|
case "property":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
currentProperty = Property(name: name, type: .void,
|
|
isReadable: attributeDict["readable"] != "0",
|
|
isWritable: attributeDict["writable"] == "1",
|
|
isConstructOnly: attributeDict["construct-only"] == "1")
|
|
|
|
case "parameter", "instance-parameter":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let transfer = TransferOwnership(rawValue: attributeDict["transfer-ownership"] ?? "none") ?? .none
|
|
let cType = attributeDict["c:type"] ?? ""
|
|
currentParameter = Parameter(name: name, type: .void, cType: cType,
|
|
isNullable: attributeDict["nullable"] == "1",
|
|
isOptional: attributeDict["optional"] == "1",
|
|
transferOwnership: transfer,
|
|
isInstanceParameter: elementName == "instance-parameter")
|
|
|
|
case "return-value":
|
|
currentReturnType = .void
|
|
|
|
case "type":
|
|
let typeName = attributeDict["name"] ?? "none"
|
|
let resolvedType = parseGIRType(typeName)
|
|
let cType = attributeDict["c:type"] ?? ""
|
|
if currentParameter != nil {
|
|
currentParameter?.type = resolvedType
|
|
if !cType.isEmpty {
|
|
currentParameter?.cType = cType
|
|
}
|
|
} else if currentReturnType != nil {
|
|
currentReturnType = resolvedType
|
|
} else if currentProperty != nil {
|
|
currentProperty?.type = resolvedType
|
|
} else if currentConstant != nil {
|
|
currentConstant?.type = resolvedType
|
|
} else if currentAlias != nil {
|
|
currentAlias?.target = resolvedType
|
|
}
|
|
|
|
case "array":
|
|
if let param = currentParameter {
|
|
currentParameter?.type = .array(param.type)
|
|
}
|
|
|
|
case "constant":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let value = requireAttribute("value", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
currentConstant = Constant(name: name, value: value, type: .void)
|
|
|
|
case "alias":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let cType = requireAttribute("c:type", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
currentAlias = Alias(name: name, cType: cType, target: .void)
|
|
|
|
case "implements":
|
|
if let name = attributeDict["name"] {
|
|
currentClass?.implements.append(name)
|
|
}
|
|
|
|
case "doc-version", "doc-deprecated", "doc-stability", "source-position":
|
|
break
|
|
|
|
case "doc":
|
|
currentText = ""
|
|
break
|
|
|
|
case "field":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let isReadable = attributeDict["readable"] != "0"
|
|
let isWritable = attributeDict["writable"] == "1"
|
|
currentRecord?.fields.append(Field(name: name, type: .void, isReadable: isReadable, isWritable: isWritable))
|
|
|
|
case "virtual-method", "parameters":
|
|
untrackedDepth += 1
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
/// Called when the XML parser encounters a closing element tag.
|
|
///
|
|
/// Finalizes the current model object, resolves the return type if one was
|
|
/// collected, appends the object to its parent container, and clears the
|
|
/// corresponding `current*` property.
|
|
/// - Parameters:
|
|
/// - parser: The XML parser.
|
|
/// - elementName: The name of the XML element.
|
|
/// - namespaceURI: The namespace URI of the element.
|
|
/// - qualifiedName: The qualified name of the element.
|
|
func parser(_ parser: XMLParser, didEndElement elementName: String,
|
|
namespaceURI: String?, qualifiedName: String?) {
|
|
switch elementName {
|
|
case "namespace":
|
|
if let ns = currentNamespace { repository.namespaces.append(ns) }
|
|
currentNamespace = nil
|
|
|
|
case "class":
|
|
if let cls = currentClass { currentNamespace?.classes.append(cls) }
|
|
currentClass = nil
|
|
|
|
case "interface":
|
|
if let iface = currentInterface { currentNamespace?.interfaces.append(iface) }
|
|
currentInterface = nil
|
|
|
|
case "record":
|
|
if let record = currentRecord { currentNamespace?.records.append(record) }
|
|
currentRecord = nil
|
|
|
|
case "enumeration":
|
|
if let enm = currentEnum { currentNamespace?.enumerations.append(enm) }
|
|
currentEnum = nil
|
|
|
|
case "bitfield":
|
|
if let bf = currentBitfield { currentNamespace?.bitfields.append(bf) }
|
|
currentBitfield = nil
|
|
|
|
case "callback":
|
|
if let cb = currentCallback {
|
|
// Only add to namespace when at the top level (not inside a class/interface/record)
|
|
if currentClass == nil && currentInterface == nil && currentRecord == nil {
|
|
currentNamespace?.callbacks.append(cb)
|
|
}
|
|
}
|
|
currentCallback = nil
|
|
|
|
case "constructor":
|
|
if var ctor = currentConstructor {
|
|
if let rt = currentReturnType, rt != .void {
|
|
ctor.returnType = rt
|
|
}
|
|
currentClass?.constructors.append(ctor)
|
|
}
|
|
currentConstructor = nil
|
|
currentReturnType = nil
|
|
|
|
case "method":
|
|
if var method = currentMethod {
|
|
if let rt = currentReturnType, rt != .void {
|
|
method.returnType = rt
|
|
}
|
|
currentClass?.methods.append(method)
|
|
currentInterface?.methods.append(method)
|
|
currentRecord?.methods.append(method)
|
|
}
|
|
currentMethod = nil
|
|
currentReturnType = nil
|
|
|
|
case "function":
|
|
if var fn = currentFunction {
|
|
if let rt = currentReturnType, rt != .void {
|
|
fn.returnType = rt
|
|
}
|
|
// Class/interface/record-level functions are not globals
|
|
if currentClass != nil {
|
|
currentClass?.functions.append(fn)
|
|
} else if currentRecord != nil {
|
|
currentRecord?.methods.append(Method(name: fn.name, cIdentifier: fn.cIdentifier,
|
|
parameters: fn.parameters, returnType: fn.returnType))
|
|
} else if currentInterface != nil {
|
|
currentInterface?.functions.append(fn)
|
|
} else {
|
|
currentNamespace?.functions.append(fn)
|
|
}
|
|
}
|
|
currentFunction = nil
|
|
currentReturnType = nil
|
|
|
|
case "signal", "glib:signal":
|
|
if var sig = currentSignal {
|
|
if let rt = currentReturnType, rt != .void {
|
|
sig.returnType = rt
|
|
}
|
|
currentClass?.signals.append(sig)
|
|
currentInterface?.signals.append(sig)
|
|
}
|
|
currentSignal = nil
|
|
currentReturnType = nil
|
|
|
|
case "property":
|
|
if let prop = currentProperty {
|
|
currentClass?.properties.append(prop)
|
|
currentInterface?.properties.append(prop)
|
|
}
|
|
currentProperty = nil
|
|
|
|
case "parameter", "instance-parameter":
|
|
if let param = currentParameter {
|
|
currentMethod?.parameters.append(param)
|
|
currentConstructor?.parameters.append(param)
|
|
currentSignal?.parameters.append(param)
|
|
currentFunction?.parameters.append(param)
|
|
currentCallback?.parameters.append(param)
|
|
}
|
|
currentParameter = nil
|
|
|
|
case "return-value":
|
|
break
|
|
|
|
case "constant":
|
|
if let c = currentConstant { currentNamespace?.constants.append(c) }
|
|
currentConstant = nil
|
|
|
|
case "alias":
|
|
if let a = currentAlias { currentNamespace?.aliases.append(a) }
|
|
currentAlias = nil
|
|
|
|
case "virtual-method", "parameters":
|
|
untrackedDepth -= 1
|
|
|
|
case "doc":
|
|
let text = currentText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !text.isEmpty else { return }
|
|
// Skip doc inside untracked elements (virtual-method, field, etc.)
|
|
guard untrackedDepth == 0 else { return }
|
|
// Check innermost (most nested) elements first to handle hierarchy
|
|
// e.g. a signal inside a class → check currentSignal before currentClass
|
|
if currentParameter != nil { currentParameter?.doc = text }
|
|
else if currentMethod != nil { currentMethod?.doc = text }
|
|
else if currentConstructor != nil { currentConstructor?.doc = text }
|
|
else if currentSignal != nil { currentSignal?.doc = text }
|
|
else if currentProperty != nil { currentProperty?.doc = text }
|
|
else if currentCallback != nil { currentCallback?.doc = text }
|
|
else if currentFunction != nil { currentFunction?.doc = text }
|
|
else if currentInterface != nil { currentInterface?.doc = text }
|
|
else if currentEnum != nil { currentEnum?.doc = text }
|
|
else if currentBitfield != nil { currentBitfield?.doc = text }
|
|
else if currentRecord != nil { currentRecord?.doc = text }
|
|
else if currentClass != nil { currentClass?.doc = text }
|
|
else if currentConstant != nil { currentConstant?.doc = text }
|
|
else if currentAlias != nil { currentAlias?.doc = text }
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
/// Called when the XML parser encounters character data between elements.
|
|
///
|
|
/// Accumulates text content for the current element, used primarily for
|
|
/// capturing `<doc>` element text content.
|
|
/// - Parameters:
|
|
/// - parser: The XML parser.
|
|
/// - string: The character data found.
|
|
func parser(_ parser: XMLParser, foundCharacters string: String) {
|
|
currentText += string
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
/// Requires that an attribute exists in the given dictionary, or aborts parsing.
|
|
/// - Parameters:
|
|
/// - key: The attribute name to look up.
|
|
/// - dict: The attribute dictionary from the current XML element.
|
|
/// - element: The name of the XML element (for error messages).
|
|
/// - parser: The XML parser to abort on failure.
|
|
/// - Returns: The attribute value, or `nil` if the attribute is missing.
|
|
private func requireAttribute(_ key: String, from dict: [String: String], for element: String, parser: XMLParser) -> String? {
|
|
guard let value = dict[key] else {
|
|
parseError = .missingAttribute("Missing '\(key)' on <\(element)>")
|
|
parser.abortParsing()
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
/// Maps a GIR type name string to the corresponding `GIRType` enum case.
|
|
///
|
|
/// Recognizes primitive GLib types (`gboolean`, `gint32`, `utf8`, etc.) and
|
|
/// dotted namespace-qualified type references (e.g. `Gtk.Widget`). Unknown
|
|
/// names are returned as an unqualified `.typeRef`.
|
|
/// - Parameter name: The GIR type name (e.g. `"gint32"`, `"utf8"`, `"Gtk.Widget"`).
|
|
/// - Returns: The corresponding `GIRType` value.
|
|
private func parseGIRType(_ name: String) -> GIRType {
|
|
switch name {
|
|
case "none": return .void
|
|
case "gboolean": return .boolean
|
|
case "gint8": return .int8
|
|
case "gint16": return .int16
|
|
case "gint", "gint32": return .int32
|
|
case "gint64": return .int64
|
|
case "guint8": return .uint8
|
|
case "guint16": return .uint16
|
|
case "guint", "guint32": return .uint32
|
|
case "guint64": return .uint64
|
|
case "gfloat": return .float
|
|
case "gdouble": return .double
|
|
case "utf8": return .string
|
|
case "filename": return .filename
|
|
case "gpointer", "gconstpointer": return .pointer
|
|
default:
|
|
if let dotIndex = name.firstIndex(of: ".") {
|
|
let ns = String(name[..<dotIndex])
|
|
let type = String(name[name.index(after: dotIndex)...])
|
|
return .typeRef(type, namespace: ns)
|
|
}
|
|
return .typeRef(name)
|
|
}
|
|
}
|
|
}
|