Compare commits

...
7 Commits
Author SHA1 Message Date
TrèsAbhi d88f7ad539 fix: arrays 2026-03-20 19:51:27 -05:00
TrèsAbhi b5d61cebb3 fix: omit non-source files 2026-03-20 19:33:43 -05:00
TrèsAbhi 551108e047 feat: some more docs 2026-03-20 19:13:37 -05:00
TrèsAbhi 5feec9bf1e fix: types 2026-03-20 14:57:00 -05:00
TrèsAbhi 313691d3e9 feat: better gutter and headings 2026-03-20 14:52:06 -05:00
TrèsAbhi 950c99d3d7 feat: clean 2026-03-20 14:48:38 -05:00
TrèsAbhi 80cc74a945 feat: typescript types 2026-03-20 14:43:12 -05:00
13 changed files with 151 additions and 78 deletions

No files matched your search

-3
View File
@@ -118,9 +118,6 @@
},
"api": {
"json": "JSON methods",
"pb": "Protocol Buffer methods",
"media": "Media methods",
"no_docs": "This method has no documentation.",
"test": "Test",
"view_full": "View full types"
@@ -44,7 +44,12 @@ for (const line of bodyOriginal.split("\n")) {
.gutter {
width: var(--space-2);
background: linear-gradient(transparent, var(--gray-6) var(--space-4));
background: linear-gradient(
transparent,
var(--gray-6) var(--space-4),
var(--gray-6) calc(100% - var(--space-4)),
transparent
);
margin: 0 var(--space-4);
}
@@ -65,9 +70,6 @@ for (const line of bodyOriginal.split("\n")) {
<div class="content">
<MarkdownCustom {body} />
<!-- {"see" in macros && <ApiMacroSee {locale} value={macros.see} />}
{"type" in macros && <ApiMacroType {locale} value={macros.type} />} -->
{
Object.entries(macroComponents).map(([macro, Component]) => {
if (!(macro in macros)) return null;
@@ -10,8 +10,4 @@ interface Props {
const { apis, locale } = Astro.props;
---
{
apis
.sort((a, b) => a.id.localeCompare(b.id))
.map((api) => <ApiEntry {locale} {api} />)
}
{apis.map((api) => <ApiEntry {locale} {api} />)}
@@ -1,5 +1,5 @@
---
import { existsSync } from "fs";
import { resolve } from "path";
import { readFile } from "fs/promises";
import { Link } from "./Link";
import { api } from "../core/api/dynamic";
@@ -7,6 +7,23 @@ import HighlightedCodeBlock from "./HighlightedCodeBlock.astro";
import { Text } from "./Text";
import { ArrowBottomRightIcon } from "@radix-ui/react-icons";
import { Code } from "./Code";
import {
createProgram,
forEachChild,
isArrayTypeNode,
isEnumDeclaration,
isExpressionWithTypeArguments,
isInterfaceDeclaration,
isMethodSignature,
isPropertySignature,
isTypeAliasDeclaration,
isTypeNode,
isTypeReferenceNode,
type InterfaceDeclaration,
type Node,
type Symbol,
type TypeNode,
} from "typescript";
interface Props {
value: string;
@@ -16,62 +33,102 @@ interface Props {
const { value, locale } = Astro.props;
const strings = await api.strings(locale);
const lang = "ts";
const roots = ["src/protos", "../closed/protos"];
const lang = "proto";
const importPattern = /import "(.+)";/gm;
const messagePattern = /message .+ {\s*\n(.+\s*\n)+}/gm;
const [route, name] = value.split(".");
const path = resolve(`${roots[0]}/${route}.ts`);
const explored = new Set<string>();
let messages: string[] = [];
const program = createProgram([path], {});
const source = program.getSourceFile(path);
await pullMessages(`${roots[0]}/${value}.proto`);
if (source === undefined) {
throw new Error(`Could not find ${path}`);
}
async function pullMessages(path: string) {
if (explored.has(path)) return;
const checker = program.getTypeChecker();
const collected = new Map<string, string>();
const visited = new Set<Symbol>();
let declaration: InterfaceDeclaration | undefined;
explored.add(path);
forEachChild(source, (node) => {
if (isInterfaceDeclaration(node) && node.name.text === name) {
declaration = node;
}
});
const file = await readFile(path);
const content = file.toString();
const imports = content.matchAll(importPattern);
if (declaration === undefined) {
throw new Error(`Could not find ${name} in ${path}`);
}
for (const _import of imports) {
for (const root of roots) {
const path = `${root}/${_import[1]}`;
function collectNodes(node: Node) {
if (isInterfaceDeclaration(node)) {
const symbol = checker.getSymbolAtLocation(node.name);
if (existsSync(path)) {
await pullMessages(path);
break;
}
if (symbol) {
const text = node.getText();
collected.set(node.name.text, text);
node.members.forEach((member) => {
if (isPropertySignature(member) && member.type) {
collectTypes(member.type);
}
});
}
}
const matchedMessages = content.matchAll(messagePattern);
forEachChild(node, collectNodes);
}
for (const message of matchedMessages) {
messages = [message[0], ...messages];
function collectTypes(node: TypeNode) {
if (isArrayTypeNode(node)) {
collectTypes(node.elementType);
return;
}
if (isTypeReferenceNode(node)) {
node.typeArguments?.forEach(collectTypes);
}
const type = checker.getTypeAtLocation(node);
const symbol = type.getSymbol();
if (!symbol || visited.has(symbol)) return;
visited.add(symbol);
const declarations = symbol.getDeclarations();
if (!declarations) return;
for (const declaration of declarations) {
if (isInterfaceDeclaration(declaration)) {
collected.set(declaration.name.text, declaration.getText());
declaration.members.forEach((member) => {
if (isPropertySignature(member) && member.type) {
collectTypes(member.type);
}
});
} else {
forEachChild(declaration, (child) => {
if (isTypeNode(child)) collectTypes(child);
});
}
}
}
const content = messages.join("\n\n");
collectNodes(declaration);
const indexPattern = /^( +).+\.(.+ = \d+;)$/m;
let content = "";
let stripped = "";
for (const line of content.split("\n")) {
const matches = line.match(indexPattern);
if (matches === null) {
stripped += `${line}\n`;
continue;
}
stripped += `${matches[1]}${matches[2]}\n`;
for (const [, definition] of collected) {
content += `${definition}\n\n`;
}
stripped = stripped.replaceAll(" ", " ");
content = content.replaceAll(/^export\s*/gm, "");
content = content.trim();
---
<style>
@@ -136,7 +193,7 @@ stripped = stripped.replaceAll(" ", " ");
</div>
<div class="types">
<HighlightedCodeBlock {lang} content={stripped} />
<HighlightedCodeBlock {lang} content={content} />
<div class="overlay">
<Link underline="always" href="#" className="view-full">
@@ -1,6 +1,7 @@
import type { ComponentProps } from "react";
import type { ComponentProps, FunctionComponent } from "react";
import Markdown from "react-markdown";
import { classNames } from "../../core/ui/classNames";
import { Code } from "../Code";
import { Text } from "../Text";
import styles from "./index.module.css";
@@ -13,7 +14,8 @@ export function MarkdownCustom({ body, className, ...props }: MarkdownCustom) {
<div className={classNames(className, styles.markdown)} {...props}>
<Markdown
components={{
p: Text,
p: Text as FunctionComponent<ComponentProps<"p">>,
code: Code,
}}
>
{body}
@@ -1,6 +1,7 @@
import type { Strings } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import type { Avatar } from "../../protos/avatar";
import type { AvatarList } from "../../protos/avatar_list";
import type { Avatars } from "../../protos/avatars";
import type { Background } from "../../protos/background";
import type { Backgrounds } from "../../protos/backgrounds";
@@ -130,6 +131,16 @@ export abstract class AbstractAPI {
return this._avatarsCache;
}
private _avatarListCache: AvatarList | undefined;
protected abstract _avatarList(): Promise<AvatarList>;
async avatarList() {
if (this._avatarListCache === undefined) {
this._avatarListCache = await this._avatarList();
}
return this._avatarListCache;
}
private _backgroundCache: Record<string, Background> = {};
protected abstract _background(id: string): Promise<Background>;
async background(id: string) {
@@ -193,6 +193,13 @@ export class ServerAPI extends AbstractAPI {
return { avatars };
}
protected async _avatarList() {
const avatars = await this.avatars();
const list = Object.keys(avatars.avatars);
return { list };
}
protected async _avatar(id: string) {
const avatar = this.metadata.item(`ProfileAvatarEntity.${id}`);
const name = avatar.name;
@@ -3,11 +3,9 @@ import "@radix-ui/colors/amber-dark.css";
import { getCollection } from "astro:content";
import ApiGroup from "../../../components/ApiGroup.astro";
import ColorScale from "../../../components/ColorScale.astro";
import { Heading } from "../../../components/Heading";
import { Section } from "../../../components/Section";
import SyntaxStyles from "../../../components/SyntaxStyles.astro";
import ToolBanner from "../../../components/ToolBanner.astro";
import { api } from "../../../core/api/dynamic";
import { tools } from "../../../core/tools/tools";
import BaseLayout from "../../../layouts/BaseLayout.astro";
@@ -15,7 +13,6 @@ export { getStaticPaths } from "../index.astro";
const { locale } = Astro.props;
const collection = await getCollection("api");
const strings = await api.strings(locale);
const paths: {
path: string;
@@ -32,28 +29,11 @@ paths.sort((a, b) => a.path.localeCompare(b.path));
<ToolBanner {locale} tool={tools.api} slot="pre-app" />
<Section>
<Heading size="2">{strings.api.json}</Heading>
<ApiGroup
{locale}
apis={collection.filter((api) => api.id.endsWith(".json"))}
/>
</Section>
<Section>
<Heading size="2">{strings.api.pb}</Heading>
<ApiGroup
{locale}
apis={collection.filter((api) => api.id.endsWith(".pb"))}
/>
</Section>
<Section>
<Heading size="2">{strings.api.media}</Heading>
<ApiGroup
{locale}
apis={collection.filter(
(api) => !api.id.endsWith(".json") && !api.id.endsWith(".pb"),
)}
apis={collection
.filter((api) => !api.id.endsWith(".pb"))
.sort((a, b) => a.id.localeCompare(b.id))}
/>
</Section>
</BaseLayout>
@@ -16,7 +16,7 @@ export const getStaticPaths = (async () => {
*
* @see avatars/list.json for all avatar ids.
* @see avatars/[id].webp for avatar images.
* @type avatar
* @type avatar.Avatar
*/
export async function GET({ params }: APIContext<never, { id: string }>) {
const avatar = await api.avatar(params.id);
@@ -4,6 +4,12 @@ import { imageProxy } from "../../../core/api/imageProxy";
export { getStaticPaths } from "./[id].json";
/**
* Returns an avatar image based on its `id` in the `webp` format.
*
* @see avatars/list.json for all avatar ids.
* @see avatars/[id].json for data on all avatar.
*/
export async function GET({ params }: APIContext<never, { id: string }>) {
const avatar = await api.avatar(params.id);
return await imageProxy(avatar.profile_avatar!.avatar);
@@ -1,5 +1,11 @@
import { api } from "../../../core/api/dynamic";
/**
* Identical to `avatars/[id].json`, but returns data on all avatars at once,
* mapped by `id`. Beware, the returned `JSON` is large.
*
* @type avatars.Avatars
*/
export async function GET() {
const avatars = await api.avatars();
return Response.json(avatars);
@@ -1,7 +1,9 @@
import { api } from "../../../core/api/dynamic";
/**
* @type avatar_list.AvatarList
*/
export async function GET() {
const { avatars } = await api.avatars();
const list = avatars.map((avatar) => avatar.name);
const list = await api.avatarList();
return Response.json(list);
}
@@ -0,0 +1,7 @@
syntax = "proto3";
package blitzkit;
message AvatarList {
repeated string list = 1;
}