// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// The schema reference: what the endpoint knows, written down. A `GET` on the
/// endpoint serves it when the `Ide` is `reference`.
///
/// It is rendered from the schema the application holds, not from JSON the page
/// fetches: the types, their fields, the arguments each field takes, the
/// descriptions and the deprecations are already in memory, so the page is one
/// string with nothing to load. That is the difference between this and the four
/// IDEs beside it — they run operations, this one is read.
///
/// The page is the same material as the SDL, arranged for a reader: the root
/// types first, then every other type grouped by kind, each type a section with
/// an anchor a link can point at.

///|
/// Escape the five characters that would otherwise end a text run or an
/// attribute early.
fn html_escape(text : String) -> String {
  let out = StringBuilder()
  for ch in text {
    match ch {
      '<' => out.write_string("<")
      '>' => out.write_string(">")
      '&' => out.write_string("&")
      '"' => out.write_string(""")
      '\'' => out.write_string("'")
      _ => out.write_char(ch)
    }
  }
  out.to_string()
}

///|
/// A link to a type's section, or the name as text when the schema does not
/// declare the type (a built-in scalar, mostly).
fn type_link(schema : @schema.Schema, name : String) -> String {
  let known = schema.type_by_name(name) is Some(_) ||
    schema.enum_by_name(name) is Some(_) ||
    schema.union_by_name(name) is Some(_) ||
    schema.scalar_by_name(name) is Some(_)
  if known {
    "" +
    html_escape(name) +
    ""
  } else {
    "" + html_escape(name) + ""
  }
}

///|
/// A type as the SDL would write it — `[User!]!` — with every named part a link
/// and the brackets carried as punctuation, so a reader can click into the type
/// a field returns.
fn type_ref_html(schema : @schema.Schema, t : @schema.GqlType) -> String {
  match t {
    Scalar(name) => type_link(schema, name)
    Named(name) => type_link(schema, name)
    NonNull(inner) =>
      type_ref_html(schema, inner) + "!"
    ListOf(inner) =>
      "[" +
      type_ref_html(schema, inner) +
      "]"
  }
}

///|
/// The fields of an object, interface or input type: one row each, with the
/// arguments a field takes spelled out under its name.
fn fields_html(
  schema : @schema.Schema,
  fields : Array[@schema.Field],
) -> String {
  if fields.is_empty() {
    return "

No fields.

" } let out = StringBuilder() out.write_string("
") for f in fields { out.write_string("
") out.write_string("
") out.write_string(html_escape(f.name)) out.write_string("") if !f.args.is_empty() { out.write_string("(") let mut first = true for a in f.args { if !first { out.write_string(", ") } first = false out.write_string("") out.write_string(html_escape(a.0)) out.write_string(": ") out.write_string(type_ref_html(schema, a.1)) match f.arg_defaults.get(a.0) { Some(d) => { out.write_string(" = ") out.write_string(html_escape(d.stringify())) out.write_string("") } None => () } } out.write_string(")") } out.write_string(": ") out.write_string(type_ref_html(schema, f.typ)) out.write_string("
") match f.description { Some(d) => out.write_string("
" + html_escape(d) + "
") None => () } match f.deprecation_reason { Some(reason) => out.write_string( "
Deprecated: " + html_escape(reason) + "
", ) None => () } out.write_string("
") } out.write_string("
") out.to_string() } ///| /// One type's section: the heading with its kind, the description, the /// interfaces it implements, and the body each kind of type has. fn type_section( schema : @schema.Schema, name : String, kind : String, description : String?, implements : Array[String], body : String, ) -> String { let out = StringBuilder() out.write_string("
") out.write_string("

" + html_escape(name) + "") out.write_string("" + html_escape(kind) + "

") match description { Some(d) => out.write_string("

" + html_escape(d) + "

") None => () } if !implements.is_empty() { out.write_string("

Implements ") let mut first = true for i in implements { if !first { out.write_string(", ") } first = false out.write_string(type_link(schema, i)) } out.write_string("

") } out.write_string(body) out.write_string("
") out.to_string() } ///| /// The stylesheet. One set of variables, a serif for prose, a mono for /// signatures, and every scrollable region — the page, the rail, a field row that /// runs long — carrying the site's own bar rather than the browser's. fn reference_style() -> String { let css = #| css } ///| /// The script: the search that narrows the rail and the page together, and the /// scrollbar that is only visible once the page has moved. fn reference_script() -> String { let script = #| script } ///| /// The documentation page for `schema`, served at `endpoint`. pub fn reference_html( schema : @schema.Schema, endpoint~ : String = "/graphql", ) -> String { let objects : Array[@schema.ObjectType] = [] let interfaces : Array[@schema.ObjectType] = [] let inputs : Array[@schema.ObjectType] = [] let mut field_count = 0 for t in schema.types { field_count = field_count + t.fields.length() match t.kind { @schema.Object => objects.push(t) @schema.Interface => interfaces.push(t) @schema.Input => inputs.push(t) } } let out = StringBuilder() out.write_string("\n\n\n") out.write_string("\n") out.write_string( "\n", ) out.write_string("Schema reference\n") out.write_string( "\n", ) out.write_string( "\n\n", ) out.write_string( "\n", ) out.write_string(reference_style()) out.write_string("\n\n") out.write_string("
\n") out.write_string("
MoonQLSchema reference
\n") out.write_string( "
" + html_escape(endpoint) + "
\n", ) out.write_string( "
\n", ) out.write_string("
\n
\n") // The rail: the root types first, because they are what a caller starts from, // then everything else by kind. out.write_string("\n
\n") out.write_string("
") out.write_string("

Everything this endpoint answers

") out.write_string( "

A rendering of the schema the server holds, in the order a caller works through it. Fields link to the type they return; deprecated ones say so. The same material is available as SDL from the schema itself.

", ) out.write_string("
") out.write_string("
") out.write_string( "
Types" + (objects.length() + interfaces.length() + inputs.length() + schema.enums.length() + schema.unions.length() + schema.scalars.length()).to_string() + "
", ) out.write_string("
Fields" + field_count.to_string() + "
") out.write_string( "
Enums" + schema.enums.length().to_string() + "
", ) out.write_string( "
Unions" + schema.unions.length().to_string() + "
", ) out.write_string("
\n") // The root types, each with its fields. for name in roots { match schema.type_by_name(name) { Some(t) => out.write_string( type_section( schema, t.name, "ROOT", t.description, t.interfaces, fields_html(schema, t.fields), ), ) None => () } } for t in sorted_objects(objects) { if roots.contains(t.name) { continue } out.write_string( type_section( schema, t.name, "OBJECT", t.description, t.interfaces, fields_html(schema, t.fields), ), ) } for t in sorted_objects(interfaces) { out.write_string( type_section( schema, t.name, "INTERFACE", t.description, t.interfaces, fields_html(schema, t.fields), ), ) } for u in schema.unions { let body = StringBuilder() body.write_string("
") for m in u.members { body.write_string("" + type_link(schema, m) + "") } body.write_string("
") out.write_string( type_section(schema, u.name, "UNION", u.description, [], body.to_string()), ) } for e in schema.enums { let body = StringBuilder() body.write_string("
") for v in e.values { body.write_string("" + html_escape(v) + "") } body.write_string("
") for v in e.values { match e.value_descriptions.get(v) { Some(d) => body.write_string( "
" + html_escape(v) + "
" + html_escape(d) + "
", ) None => () } } out.write_string( type_section(schema, e.name, "ENUM", e.description, [], body.to_string()), ) } for t in sorted_objects(inputs) { out.write_string( type_section( schema, t.name, "INPUT OBJECT", t.description, [], fields_html(schema, t.fields), ), ) } for s in schema.scalars { let body = match s.spec_url { Some(u) => "

Specification: " + html_escape(u) + "

" None => "" } out.write_string( type_section(schema, s.name, "SCALAR", s.description, [], body), ) } out.write_string( "
query " + html_escape(schema.query) + root_note(schema) + "
", ) out.write_string("
\n
\n") out.write_string(reference_script()) out.write_string("\n\n") out.to_string() } ///| /// Names in dictionary order, the way the rail is meant to read. fn dict_order(names : Array[String]) -> Array[String] { let out = names.copy() out.sort_by(fn(a, b) { a.compare(b) }) out } ///| /// The names of the three root types that exist, in the order a document /// addresses them. fn root_names(schema : @schema.Schema) -> Array[String] { let out : Array[String] = [schema.query] match schema.mutation { Some(m) => out.push(m) None => () } match schema.subscription { Some(s) => out.push(s) None => () } out } ///| /// The mutation and subscription roots the schema also declares, spelled out /// under the query root's name. fn root_note(schema : @schema.Schema) -> String { let out = StringBuilder() match schema.mutation { Some(m) => out.write_string(" · mutation " + html_escape(m)) None => () } match schema.subscription { Some(s) => out.write_string(" · subscription " + html_escape(s)) None => () } out.to_string() } ///| /// Object types in dictionary order, the order the sections are written in. fn sorted_objects( types : Array[@schema.ObjectType], ) -> Array[@schema.ObjectType] { let out = types.copy() out.sort_by(fn(a, b) { a.name.compare(b.name) }) out } ///| /// The page a reference request is answered with when the application did not /// hand over a schema: the same chrome, and one sentence saying so instead of a /// documentation page with nothing in it. fn missing_schema_page(endpoint : String) -> String { let out = StringBuilder() out.write_string("\n\n\n") out.write_string("\n") out.write_string( "\n", ) out.write_string("Schema reference\n") out.write_string(reference_style()) out.write_string( "\n\n
MoonQL", ) out.write_string("Schema reference
") out.write_string(html_escape(endpoint)) out.write_string( "
\n
", ) out.write_string("

No schema to show

") out.write_string( "

This page is rendered from the schema the application holds, and none was passed to it. Serving it through the application's own endpoint (graphql_app or graphql_app_async) supplies one.

", ) out.write_string("
\n\n\n") out.to_string() }