-8
.tangled/workflows/publish-docs.yml
-8
.tangled/workflows/publish-docs.yml
···
1
1
when:
2
2
- event: push
3
3
tag: "v*"
4
-
- event: push
5
-
branch: main
6
-
path:
7
-
- "README.md"
8
-
- "CHANGELOG.md"
9
-
- "docs/**"
10
-
- "devlog/**"
11
4
12
5
engine: nixery
13
6
···
18
11
steps:
19
12
- name: build and publish docs to ATProto
20
13
command: |
21
-
test -n "$ATPROTO_PASSWORD"
22
14
zig build
23
15
./zig-out/bin/publish-docs
-196
scripts/publish-docs.zig
-196
scripts/publish-docs.zig
···
82
82
83
83
try putRecord(&client, allocator, session.did, "site.standard.document", tid.str(), doc_record);
84
84
std.debug.print("published: {s} -> at://{s}/site.standard.document/{s}\n", .{ doc.file, session.did, tid.str() });
85
-
86
-
// also publish to leaflet
87
-
const leaflet_blocks = try parseMarkdownToLeafletBlocks(allocator, content);
88
-
const leaflet_page = LeafletPage{ .blocks = leaflet_blocks };
89
-
const leaflet_pages = try allocator.alloc(LeafletPage, 1);
90
-
leaflet_pages[0] = leaflet_page;
91
-
92
-
const leaflet_record = LeafletDocument{
93
-
.author = session.did,
94
-
.title = title,
95
-
.publishedAt = &now,
96
-
.pages = leaflet_pages,
97
-
};
98
-
try putRecord(&client, allocator, session.did, "pub.leaflet.document", tid.str(), leaflet_record);
99
-
std.debug.print("published: {s} -> at://{s}/pub.leaflet.document/{s}\n", .{ doc.file, session.did, tid.str() });
100
85
}
101
86
102
87
// devlog publication (clock_id 100 to separate from docs)
···
136
121
137
122
try putRecord(&client, allocator, session.did, "site.standard.document", tid.str(), doc_record);
138
123
std.debug.print("published: {s} -> at://{s}/site.standard.document/{s}\n", .{ entry.file, session.did, tid.str() });
139
-
140
-
// also publish to leaflet
141
-
const leaflet_blocks = try parseMarkdownToLeafletBlocks(allocator, content);
142
-
const leaflet_page = LeafletPage{ .blocks = leaflet_blocks };
143
-
const leaflet_pages = try allocator.alloc(LeafletPage, 1);
144
-
leaflet_pages[0] = leaflet_page;
145
-
146
-
const leaflet_record = LeafletDocument{
147
-
.author = session.did,
148
-
.title = title,
149
-
.publishedAt = &now,
150
-
.pages = leaflet_pages,
151
-
};
152
-
try putRecord(&client, allocator, session.did, "pub.leaflet.document", tid.str(), leaflet_record);
153
-
std.debug.print("published: {s} -> at://{s}/pub.leaflet.document/{s}\n", .{ entry.file, session.did, tid.str() });
154
124
}
155
125
156
126
std.debug.print("done\n", .{});
···
172
142
publishedAt: []const u8,
173
143
};
174
144
175
-
// leaflet types
176
-
const LeafletDocument = struct {
177
-
@"$type": []const u8 = "pub.leaflet.document",
178
-
author: []const u8,
179
-
title: []const u8,
180
-
publishedAt: ?[]const u8 = null,
181
-
pages: []const LeafletPage,
182
-
};
183
-
184
-
const LeafletPage = struct {
185
-
@"$type": []const u8 = "pub.leaflet.pages.linearDocument",
186
-
blocks: []const LeafletBlockWrapper,
187
-
};
188
-
189
-
const LeafletBlockWrapper = struct {
190
-
block: LeafletBlock,
191
-
};
192
-
193
-
const LeafletBlock = union(enum) {
194
-
header: HeaderBlock,
195
-
text: TextBlock,
196
-
code: CodeBlock,
197
-
198
-
pub fn jsonStringify(self: @This(), jw: anytype) !void {
199
-
switch (self) {
200
-
.header => |h| try jw.write(h),
201
-
.text => |t| try jw.write(t),
202
-
.code => |c| try jw.write(c),
203
-
}
204
-
}
205
-
};
206
-
207
-
const HeaderBlock = struct {
208
-
@"$type": []const u8 = "pub.leaflet.blocks.header",
209
-
plaintext: []const u8,
210
-
level: u8,
211
-
};
212
-
213
-
const TextBlock = struct {
214
-
@"$type": []const u8 = "pub.leaflet.blocks.text",
215
-
plaintext: []const u8,
216
-
};
217
-
218
-
const CodeBlock = struct {
219
-
@"$type": []const u8 = "pub.leaflet.blocks.code",
220
-
plaintext: []const u8,
221
-
language: ?[]const u8 = null,
222
-
};
223
-
224
145
const Session = struct {
225
146
did: []const u8,
226
147
access_token: []const u8,
···
340
261
}) catch unreachable;
341
262
return buf;
342
263
}
343
-
344
-
/// parse markdown into leaflet blocks (minimal: headers, code, paragraphs)
345
-
fn parseMarkdownToLeafletBlocks(allocator: Allocator, markdown: []const u8) ![]LeafletBlockWrapper {
346
-
var blocks: std.ArrayList(LeafletBlockWrapper) = .empty;
347
-
errdefer blocks.deinit(allocator);
348
-
349
-
var lines = std.mem.splitScalar(u8, markdown, '\n');
350
-
var in_code_block = false;
351
-
var code_content: std.ArrayList(u8) = .empty;
352
-
defer code_content.deinit(allocator);
353
-
var code_lang: ?[]const u8 = null;
354
-
var paragraph_lines: std.ArrayList([]const u8) = .empty;
355
-
defer paragraph_lines.deinit(allocator);
356
-
357
-
while (lines.next()) |line| {
358
-
const trimmed = std.mem.trim(u8, line, " \t\r");
359
-
360
-
// handle code blocks
361
-
if (std.mem.startsWith(u8, trimmed, "```")) {
362
-
if (in_code_block) {
363
-
// end code block
364
-
try blocks.append(allocator, .{ .block = .{ .code = .{
365
-
.plaintext = try allocator.dupe(u8, code_content.items),
366
-
.language = code_lang,
367
-
} } });
368
-
code_content.clearRetainingCapacity();
369
-
code_lang = null;
370
-
in_code_block = false;
371
-
} else {
372
-
// flush any pending paragraph first
373
-
if (paragraph_lines.items.len > 0) {
374
-
const para_text = try std.mem.join(allocator, " ", paragraph_lines.items);
375
-
if (para_text.len > 0) {
376
-
try blocks.append(allocator, .{ .block = .{ .text = .{ .plaintext = para_text } } });
377
-
}
378
-
paragraph_lines.clearRetainingCapacity();
379
-
}
380
-
// start code block
381
-
in_code_block = true;
382
-
const lang_part = trimmed[3..];
383
-
if (lang_part.len > 0) {
384
-
code_lang = try allocator.dupe(u8, lang_part);
385
-
}
386
-
}
387
-
continue;
388
-
}
389
-
390
-
if (in_code_block) {
391
-
if (code_content.items.len > 0) {
392
-
try code_content.append(allocator, '\n');
393
-
}
394
-
try code_content.appendSlice(allocator, line);
395
-
continue;
396
-
}
397
-
398
-
// handle headers
399
-
if (trimmed.len > 0 and trimmed[0] == '#') {
400
-
// flush any pending paragraph first
401
-
if (paragraph_lines.items.len > 0) {
402
-
const para_text = try std.mem.join(allocator, " ", paragraph_lines.items);
403
-
if (para_text.len > 0) {
404
-
try blocks.append(allocator, .{ .block = .{ .text = .{ .plaintext = para_text } } });
405
-
}
406
-
paragraph_lines.clearRetainingCapacity();
407
-
}
408
-
409
-
var level: u8 = 0;
410
-
for (trimmed) |c| {
411
-
if (c == '#') level += 1 else break;
412
-
}
413
-
if (level > 0 and level <= 6 and trimmed.len > level and trimmed[level] == ' ') {
414
-
var header_text = trimmed[level + 1 ..];
415
-
// strip markdown link: [text](url) -> text
416
-
if (std.mem.indexOf(u8, header_text, "](")) |bracket| {
417
-
if (header_text[0] == '[') {
418
-
header_text = header_text[1..bracket];
419
-
}
420
-
}
421
-
try blocks.append(allocator, .{ .block = .{ .header = .{
422
-
.plaintext = try allocator.dupe(u8, header_text),
423
-
.level = level,
424
-
} } });
425
-
continue;
426
-
}
427
-
}
428
-
429
-
// blank line ends paragraph
430
-
if (trimmed.len == 0) {
431
-
if (paragraph_lines.items.len > 0) {
432
-
const para_text = try std.mem.join(allocator, " ", paragraph_lines.items);
433
-
if (para_text.len > 0) {
434
-
try blocks.append(allocator, .{ .block = .{ .text = .{ .plaintext = para_text } } });
435
-
}
436
-
paragraph_lines.clearRetainingCapacity();
437
-
}
438
-
continue;
439
-
}
440
-
441
-
// accumulate paragraph lines
442
-
try paragraph_lines.append(allocator, try allocator.dupe(u8, trimmed));
443
-
}
444
-
445
-
// flush remaining content
446
-
if (in_code_block and code_content.items.len > 0) {
447
-
try blocks.append(allocator, .{ .block = .{ .code = .{
448
-
.plaintext = try allocator.dupe(u8, code_content.items),
449
-
.language = code_lang,
450
-
} } });
451
-
} else if (paragraph_lines.items.len > 0) {
452
-
const para_text = try std.mem.join(allocator, " ", paragraph_lines.items);
453
-
if (para_text.len > 0) {
454
-
try blocks.append(allocator, .{ .block = .{ .text = .{ .plaintext = para_text } } });
455
-
}
456
-
}
457
-
458
-
return blocks.toOwnedSlice(allocator);
459
-
}