git » sdk » commit ced862b

Generate proper swift API docs

author Stephen Paul Weber
2026-04-09 04:32:46 UTC
committer Stephen Paul Weber
2026-04-09 04:32:46 UTC
parent 14882d447317f59f8c229d0f6c8564f474d4ed85

Generate proper swift API docs

Docc2Mkdocs.hx +287 -0
docs/swift/index.md +120 -0
mkdocs.yml +2 -1

diff --git a/Docc2Mkdocs.hx b/Docc2Mkdocs.hx
new file mode 100644
index 0000000..07e5294
--- /dev/null
+++ b/Docc2Mkdocs.hx
@@ -0,0 +1,287 @@
+import haxe.Json;
+import haxe.io.Path;
+import sys.FileSystem;
+import sys.io.File;
+
+typedef DoccAbstractFragment = {
+	var text:String;
+}
+
+typedef DoccReference = {
+	@:optional var fragments:Array<Dynamic>;
+	var title:String;
+	var url:String;
+}
+
+typedef DoccTopicSection = {
+	var identifiers:Array<String>;
+	var title:String;
+}
+
+typedef DoccDocument = {
+	@:optional var references:Dynamic<DoccReference>;
+	@:optional var topicSections:Array<DoccTopicSection>;
+}
+
+class Docc2Mkdocs {
+	static function main():Void {
+		var args = Sys.args();
+		var inputRoot = "doc/data/documentation";
+		var outputRoot = "doc/data/documentation-makedoc";
+		var index = 0;
+
+		while (index < args.length) {
+			switch (args[index]) {
+				case "--input":
+					index++;
+					ensureHasValue(args, index, "--input");
+					inputRoot = args[index];
+				case "--output":
+					index++;
+					ensureHasValue(args, index, "--output");
+					outputRoot = args[index];
+				case "--help", "-h":
+					printUsage();
+					return;
+				case other:
+					Sys.println("Unknown argument: " + other);
+					printUsage();
+					Sys.exit(1);
+			}
+
+			index++;
+		}
+
+		inputRoot = Path.normalize(inputRoot);
+		outputRoot = Path.normalize(outputRoot);
+
+		if (!FileSystem.exists(inputRoot) || !FileSystem.isDirectory(inputRoot)) {
+			fail("Input directory does not exist: " + inputRoot);
+		}
+
+		resetDirectory(outputRoot);
+		processDirectory(inputRoot, outputRoot, inputRoot);
+	}
+
+	static function processDirectory(inputDir:String, outputDir:String, docRoot:String):Void {
+		ensureDirectory(outputDir);
+
+		var entries = FileSystem.readDirectory(inputDir);
+		entries.sort(Reflect.compare);
+
+		for (entry in entries) {
+			var sourcePath = Path.join([inputDir, entry]);
+			var destPath = Path.join([outputDir, entry]);
+
+			if (FileSystem.isDirectory(sourcePath)) {
+				processDirectory(sourcePath, destPath, docRoot);
+				continue;
+			}
+
+			if (!StringTools.endsWith(entry, ".md")) {
+				continue;
+			}
+
+			var original = File.getContent(sourcePath);
+			var relPath = Path.normalize(sourcePath.substr(docRoot.length + 1));
+			var rendered = augmentMarkdown(original, sourcePath, relPath, docRoot);
+			File.saveContent(destPath, rendered);
+		}
+	}
+
+	static function augmentMarkdown(original:String, sourceMdPath:String, relPath:String, docRoot:String):String {
+		var jsonPath = sourceMdPath.substr(0, sourceMdPath.length - 3) + ".json";
+		if (!FileSystem.exists(jsonPath)) {
+			return original;
+		}
+
+		var parsed:DoccDocument = cast Json.parse(File.getContent(jsonPath));
+		if (parsed.topicSections == null || parsed.topicSections.length == 0 || parsed.references == null) {
+			return original;
+		}
+
+		var generatedSections = renderTopicSections(parsed, relPath);
+		if (generatedSections.length == 0) {
+			return original;
+		}
+
+		var trimmed = StringTools.rtrim(original);
+		return trimmed + "\n\n" + generatedSections.join("\n\n") + "\n";
+	}
+
+	static function renderTopicSections(parsed:DoccDocument, currentRelPath:String):Array<String> {
+		var sections = new Array<String>();
+
+		for (topicSection in parsed.topicSections) {
+			if (topicSection.identifiers == null || topicSection.identifiers.length == 0) {
+				continue;
+			}
+
+			var lines = ['## ' + topicSection.title, ""];
+			var addedCount = 0;
+
+			for (identifier in topicSection.identifiers) {
+				var reference:DoccReference = Reflect.field(parsed.references, identifier);
+				if (reference == null || reference.url == null || reference.title == null) {
+					continue;
+				}
+
+				var bullet = renderReferenceBullet(reference, currentRelPath);
+				if (bullet == null) {
+					continue;
+				}
+
+				lines.push(bullet);
+				addedCount++;
+			}
+
+			if (addedCount > 0) {
+				sections.push(lines.join("\n"));
+			}
+		}
+
+		return sections;
+	}
+
+	static function renderReferenceBullet(reference:DoccReference, currentRelPath:String):Null<String> {
+		var targetRelPath = relativeDocPathFromUrl(reference.url, currentRelPath);
+		if (targetRelPath == null) {
+			return null;
+		}
+
+		var parts = ['- [' + reference.title + '](' + targetRelPath + ')'];
+		var summary = renderAbstractFirstLine(cast Reflect.field(reference, "abstract"));
+		if (summary != null) {
+			parts.push(summary);
+		}
+
+		return parts.join(" ");
+	}
+
+	static function relativeDocPathFromUrl(url:String, currentRelPath:String):Null<String> {
+		var normalizedUrl = StringTools.trim(url);
+		if (!StringTools.startsWith(normalizedUrl, "/documentation/")) {
+			return null;
+		}
+
+		var docRelPath = normalizedUrl.substr("/documentation/".length) + ".md";
+		var currentDir = Path.directory(currentRelPath);
+		return makeRelativePath(docRelPath, currentDir);
+	}
+
+	static function makeRelativePath(target:String, fromDir:String):String {
+		var targetParts = normalizedParts(target);
+		var fromParts = normalizedParts(fromDir);
+		var shared = 0;
+		var maxShared = targetParts.length < fromParts.length ? targetParts.length : fromParts.length;
+
+		while (shared < maxShared && targetParts[shared] == fromParts[shared]) {
+			shared++;
+		}
+
+		var relativeParts = new Array<String>();
+		for (_ in shared...fromParts.length) {
+			relativeParts.push("..");
+		}
+		for (index in shared...targetParts.length) {
+			relativeParts.push(targetParts[index]);
+		}
+
+		return relativeParts.length == 0 ? "." : relativeParts.join("/");
+	}
+
+	static function normalizedParts(path:String):Array<String> {
+		var normalized = Path.normalize(path);
+		var rawParts = normalized.split("/");
+		var parts = new Array<String>();
+
+		for (part in rawParts) {
+			if (part == "" || part == ".") {
+				continue;
+			}
+			parts.push(part);
+		}
+
+		return parts;
+	}
+
+	static function renderAbstractFirstLine(fragments:Array<DoccAbstractFragment>):Null<String> {
+		if (fragments == null || fragments.length == 0) {
+			return null;
+		}
+
+		var buffer = new StringBuf();
+		for (fragment in fragments) {
+			if (fragment != null && fragment.text != null) {
+				buffer.add(fragment.text);
+			}
+		}
+
+		var summary = firstLine(buffer.toString());
+		return summary == "" ? null : summary;
+	}
+
+	static function firstLine(value:String):String {
+		var trimmed = StringTools.trim(value);
+		if (trimmed == "") {
+			return "";
+		}
+
+		var newlineIndex = trimmed.indexOf("\n");
+		if (newlineIndex >= 0) {
+			trimmed = trimmed.substr(0, newlineIndex);
+		}
+
+		return ~/[\t ]+/.replace(StringTools.trim(trimmed), " ");
+	}
+
+	static function resetDirectory(path:String):Void {
+		if (FileSystem.exists(path)) {
+			deleteRecursively(path);
+		}
+		ensureDirectory(path);
+	}
+
+	static function deleteRecursively(path:String):Void {
+		if (FileSystem.isDirectory(path)) {
+			for (entry in FileSystem.readDirectory(path)) {
+				deleteRecursively(Path.join([path, entry]));
+			}
+			FileSystem.deleteDirectory(path);
+			return;
+		}
+
+		FileSystem.deleteFile(path);
+	}
+
+	static function ensureDirectory(path:String):Void {
+		if (path == "" || path == ".") {
+			return;
+		}
+
+		if (FileSystem.exists(path)) {
+			return;
+		}
+
+		var parent = Path.directory(path);
+		if (parent != path) {
+			ensureDirectory(parent);
+		}
+		FileSystem.createDirectory(path);
+	}
+
+	static function ensureHasValue(args:Array<String>, index:Int, flag:String):Void {
+		if (index >= args.length) {
+			fail("Missing value for " + flag);
+		}
+	}
+
+	static function printUsage():Void {
+		Sys.println("Usage: haxe --run Docc2Mkdocs [--input <dir>] [--output <dir>]");
+	}
+
+	static function fail(message:String):Void {
+		Sys.println(message);
+		Sys.exit(1);
+	}
+}
diff --git a/docs/swift/index.md b/docs/swift/index.md
new file mode 100644
index 0000000..2b4dca9
--- /dev/null
+++ b/docs/swift/index.md
@@ -0,0 +1,120 @@
+# Getting Started
+
+This guide quickly brings you up to speed on Borogove's Swift API. The API is flexible and allows you to build any type of chat or messaging app.
+
+Add the Swift package in Xcode with **Add Package Dependencies** or in `Package.swift`:
+
+```swift
+dependencies: [
+	.package(url: "https://borogove.dev/src/r/swiftpm")
+]
+```
+
+Then import the library:
+
+```swift
+import Borogove
+```
+
+## Chat Client
+
+Let's get started by initializing the SDK, creating a persistence layer, and creating a client for the current account:
+
+```swift
+import Borogove
+
+setup { message in
+	if let message {
+		print(String(cString: message))
+	}
+}
+
+let mediaStore = MediaStoreFS(path: "media")
+let persistence = Sqlite(dbfile: "borogove.db", media: mediaStore)
+let client = Client(accountId: "alice@example.com", persistence: persistence)
+```
+
+Now connect to the server and authenticate the user:
+
+```swift
+client.addPasswordNeededListener { client in
+	client.usePassword(password: "mycoolpassword")
+}
+
+client.start()
+```
+
+In a real app you will usually prompt the user when this event fires. You should not store the password yourself, as Borogove persists the user's session token until an explicit logout.
+
+## Chats
+
+Let's continue by starting your first chat. A chat contains messages and a list of people participating in it. The example below starts a chat with a new contact:
+
+```swift
+func findOneChat(client: Client) async -> Chat? {
+	for await availableChat in client.findAvailableChats(q: "hatter@example.com") {
+		return client.startChat(availableChat: availableChat)
+	}
+
+	return nil
+}
+```
+
+[`findAvailableChats`](./borogove/client/findavailablechats(q:).md) returns an async sequence of search results. Here we return the first result that is found.
+
+You can always search by the full ID or URI of any chat on the network. Locally known chats will also be returned, as well as any chats from other services configured on the account.
+
+If you have already used a chat before, you can always get it from [`getChat`](./borogove/client/getchat(chatid:).md) or list all known chats with [`getChats`](./borogove/client/getchats().md).
+
+## Messages
+
+Now that we have the chat set up, let's send our first message:
+
+```swift
+let outgoing = ChatMessageBuilder()
+outgoing.text = "I would like some tea."
+chat.sendMessage(message: outgoing)
+```
+
+You can also load the most recent messages from a chat's history:
+
+```swift
+let messages = await chat.getMessagesBefore(before: nil)
+```
+
+and send a reply to one of those:
+
+```swift
+let reply = messages[0].reply()
+reply.text = "Is that so?"
+chat.sendMessage(message: reply)
+```
+
+and mark off that you've read all of these:
+
+```swift
+if let last = messages.last {
+	chat.markReadUpTo(message: last)
+}
+```
+
+## Events
+
+This is how you can listen to events:
+
+```swift
+let onlineEventToken = client.addStatusOnlineListener {
+	print("\(client.accountId()) is online and in sync")
+}
+
+let messageEventToken = client.addChatMessageListener { message, eventType in
+	print("Message \(message.text ?? "") received or updated: \(eventType)")
+}
+```
+
+Listeners return a token that can later be removed:
+
+```swift
+client.removeEventListener(token: onlineEventToken)
+client.removeEventListener(token: messageEventToken)
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index a5e0e07..334227a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -32,6 +32,7 @@ nav:
     - js/index.md
     - API Reference: js/borogove.md
   - Swift:
-    - SwiftPM: ../src/r/swiftpm/
+    - swift/index.md
+    - API Reference: swift/borogove.md
   - Haxe:
     - API Reference: haxe/