feat(syntax): parse the apis container block
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -171,3 +171,51 @@ export function parseApiSections(source: string): ApiSections | null {
|
||||
export function hasRequestSection(source: string): boolean {
|
||||
return scanTopLevelBlocks(source, SECTION_NAMES).has("request");
|
||||
}
|
||||
|
||||
const emptySections: ApiSections = { parameters: [], body: [], response: "", error: "" };
|
||||
|
||||
/** Shape of an entry parsed from an `apis { }` container body. */
|
||||
export interface ApiEntry {
|
||||
mode: "any";
|
||||
name: string;
|
||||
method: string;
|
||||
path: string;
|
||||
body: string;
|
||||
sections: ApiSections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the body of a page-level `apis { }` container: a sequence of
|
||||
* `<name> <METHOD> <path> { ... }` entries with no leading `api` keyword
|
||||
* (the container supplies it). Each entry's braces are sliced with the
|
||||
* tokenizer's own `Lexer.readBalancedBraces()`, so the same string- and
|
||||
* comment-aware rules that protect `parseApiSections` apply here too.
|
||||
*/
|
||||
export function parseApiEntries(source: string): ApiEntry[] {
|
||||
const entries: ApiEntry[] = [];
|
||||
const lx = new Lexer(source);
|
||||
|
||||
while (lx.peek().type !== "eof") {
|
||||
const nameToken = lx.next();
|
||||
if (nameToken.type !== "ident") {
|
||||
throw new LexError(`Expected an api entry name at offset ${nameToken.pos}`);
|
||||
}
|
||||
const methodToken = lx.next();
|
||||
if (methodToken.type !== "ident") {
|
||||
throw new LexError(`Expected an HTTP method after "${nameToken.value}"`);
|
||||
}
|
||||
const path = lx.readPath();
|
||||
const entryBody = lx.readBalancedBraces();
|
||||
|
||||
entries.push({
|
||||
mode: "any",
|
||||
name: nameToken.value,
|
||||
method: methodToken.value.toUpperCase(),
|
||||
path,
|
||||
body: "",
|
||||
sections: parseApiSections(entryBody) ?? emptySections,
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { WRN_RUNTIME_TARGETS } from "./spec.ts";
|
||||
import { parseApiSections, hasRequestSection, type ApiSections } from "./api-sections.ts";
|
||||
import {
|
||||
parseApiSections,
|
||||
parseApiEntries,
|
||||
hasRequestSection,
|
||||
type ApiSections,
|
||||
} from "./api-sections.ts";
|
||||
|
||||
/**
|
||||
* Recursive-descent parser for `.wrn`, producing a small AST.
|
||||
@@ -142,7 +147,7 @@ export interface ApiBlock {
|
||||
|
||||
export type SeoBlock = Record<string, string>;
|
||||
|
||||
export type DataMode = "ssr" | "client";
|
||||
export type DataMode = "ssr" | "client" | "any";
|
||||
|
||||
export interface DataApiBlock {
|
||||
mode: DataMode;
|
||||
@@ -845,6 +850,17 @@ export function parse(source: string): PageAst {
|
||||
persist = parsePersist(lx.readBalancedBraces());
|
||||
break;
|
||||
}
|
||||
case "apis": {
|
||||
lx.next();
|
||||
const body = lx.readBalancedBraces();
|
||||
for (const entry of parseApiEntries(body)) {
|
||||
if (dataApis.some((block) => block.name === entry.name)) {
|
||||
throw new ParseError(`Duplicate api entry "${entry.name}" in apis block`);
|
||||
}
|
||||
dataApis.push(entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ParseError(`Unknown page member '${kw.value}' at offset ${kw.pos}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user