Skip to content

Commit

Permalink
Add internal links to XenAPI reference (xapi-project#6315)
Browse files Browse the repository at this point in the history
Adds a simple parser for Xapi type expressions that is used to rewrite
the types shown in the XenAPI class reference to include links to
relevant documentation.

---

![image](https://github.com/user-attachments/assets/b1a515b3-6824-4ba8-a925-9989a8cc7270)

---

The parser is structured in the form of a Pratt parser. This may seem
like overkill, but it keeps the door open to extension. Also, the parser
must work with limited information (it has no knowledge of XenAPI object
names). It works by noting that object types are always (?) suffixed by
a type constructor, e.g. `VM ref` - therefore, it assumes any prefix
form must be a valid class name, then subsequent left denotations take
the left as a type parameter (for which all are unary, except `map`
which has an alternative syntax that is special cased).

Currently, the only interesting parts of the "rendered" type are:
- Enum names become links that should scroll and temporarily highlight
(flash) the relevant details.
- Object names become links to their relevant page in the documentation.

However, other structure is retained, such as where builtin (primitive)
types are (e.g. int, bool, string, etc.), constructors names (ref, set,
option, etc.). In future, these could link to relevant portions of a new
article that explains all the types (for example, the format of datetime
is perhaps non-obvious to someone reading the XenAPI reference pages).
  • Loading branch information
contificate authored Feb 21, 2025
2 parents 1131ecb + 8583c5e commit df42cde
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 5 deletions.
4 changes: 4 additions & 0 deletions doc/assets/css/xenapi.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,7 @@ th { text-align: left;
margin: 0;
vertical-align: middle;
}

div[id$='_details'] {
cursor: default;
}
146 changes: 146 additions & 0 deletions doc/assets/js/parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@

class Type {};

class Builtin extends Type {
constructor(name) {
super();
this.name = name;
}

static ofString(s) {
const concrete = ['string', 'bool', 'int', 'float', 'void', 'datetime'];
if (!concrete.includes(s))
return null;

return new Builtin(s);
}
};

class Enum extends Type {
constructor(name) {
super();
this.name = name;
}
};

class Ctor extends Type {
constructor(params, name) {
super();
this.params = params;
this.name = name;
}
};

function lex(str) {
if (str.indexOf('$') >= 0)
throw new Error('Not allowed to contain $');

let ts = str.replaceAll('(', ' ( ');
ts = ts.replaceAll(')', ' ) ');
ts = ts.split(' ');
ts = ts.filter(x => x !== '');
ts.push('$');
return ts;
}

class Lexer {
constructor(tokens) {
this.tokens = tokens;
this.pos = 0;
}

shift() {
if (this.pos >= this.tokens.length - 1)
return '$';

return this.tokens[this.pos++];
}

peek() {
const prev = this.pos;
let t = this.shift();
this.pos = prev;
return t;
}

expect(ts) {
if (!Array.isArray(ts))
ts = [ts];

let l = this.shift();
for (const t of ts)
if (l == t) return;

throw new Error(`Expected ${t}, got ${l}`);
}
};

function lbp(t) {
switch (t) {
case '(':
case ')':
case '->':
case '\u2192':
return 0;
case '$':
return -1;
}

return 1;
}

function nud(l, t) {
switch (t) {
case 'enum':
return new Enum(l.shift());

case '(':
let left = parseType(l, 0);
l.expect(['->', '\u2192']);
let right = parseType(l, 0);
l.expect(')');
l.expect('map');
return new Ctor([left, right], 'map');
}

let bty = Builtin.ofString(t);
if (bty != null)
return bty;

const fmt = /^[a-zA-Z_]+$/;
if (fmt.test(t))
return new Ctor([], t);

throw new Error(`No null denotation for ${t}`);
}

function led(l, left, t) {
const known = ['set', 'ref', 'option', 'record'];
if (!known.includes(t))
throw new Error(`Invalid type constructor: ${t}`);

return new Ctor([left], t);
}

function parseType(l, rbp) {
let left = nud(l, l.shift());

while (lbp(l.peek()) > rbp)
left = led(l, left, l.shift());

return left;
}

function parseSingleType(input) {
try {
let lexer = new Lexer(lex(input));
let ty = parseType(lexer, 0);
if (lexer.peek() != '$')
throw new Error('Did not consume entire input');
return ty;
} catch (e) {
}

return null;
}

69 changes: 64 additions & 5 deletions doc/layouts/partials/content.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@
{{ $c := .Page.Params.class }}
{{ with index (where $.Site.Data.xenapi "name" $c) 0 }}

<script>

function render(t) {
if (t instanceof Builtin)
return `<span class="ty builtin ${t.name}">${t.name}</span>`;

if (t instanceof Enum)
return `<span class="ty enum"><span class="kw-enum">enum</span> <span class="enum name"><a href="#" onClick="locateEnum('${t.name}')">${t.name}</a></span></span>`;

if (t instanceof Ctor) {
if (t.name == 'map') {
let l = render(t.params[0]);
let r = render(t.params[1]);
return `<span class="ty ctor">(${l} \u2192 ${r}) <span class="ctor name ${t.name}">${t.name}</span></span>`;
}

if (t.params.length == 0)
return `<a href="../${t.name.toLowerCase()}" onClick="event.stopPropagation();">${t.name}</a>`;

let unary = render(t.params[0]);
return `<span class="ty ctor">${unary} <span class="ctor name ${t.name}">${t.name}</span></span>`;
}
}

function renderType(input) {
let ty = parseSingleType(input);
if (ty == null)
return input;

return render(ty);
}
</script>

<script type="text/javascript">
function showhide(obj) {
if (obj.style.display == 'none')
Expand All @@ -16,6 +49,21 @@
obj.style.display = 'none';
}

function toggle(e) {
showhide(e.nextElementSibling);
}

function locateEnum(name) {
event.stopPropagation();
let target = document.querySelector(`#enum_${name}`);
document.querySelector('#enums').scrollIntoView();
let detail = target.querySelector(`#enum_${name}_details`);
detail.style.display = 'inherit';
target.style.transition = '0.1s';
target.style.textShadow = '0px 4px 5px rgba(0, 0, 0, 0.5)';
setTimeout(() => { target.style.textShadow = 'none'; }, 300);
}

function toggle_implicit(button) {
var elements = document.querySelectorAll(".implicit");
for (element of elements)
Expand All @@ -29,7 +77,9 @@
</script>

{{ $style := resources.Get "css/xenapi.css" }}
{{ $parser := resources.Get "js/parse.js" }}
<link rel="stylesheet" href="{{ $style.Permalink }}">
<script src="{{ $parser.Permalink }}"></script>

{{ with .lifecycle }}
<div class="lifecycle">
Expand Down Expand Up @@ -64,11 +114,11 @@ <h2 class="title" onclick="showhide(document.getElementById('class_{{$c}}_detail
</div>

{{ if gt (len .enums) 0 }}
<h3>Enums</h3>
<h3 id="enums">Enums</h3>

{{ range $i, $x := .enums }}
<div id="enum_{{$x.name}}" class="{{ if modBool $i 2 }}field{{ else }}field2{{ end }}" >
<div class="field-name" onclick="showhide(document.getElementById('enum_{{$x.name}}_details'))">{{ $x.name }}</div>
<div class="field-name" onclick="toggle(this)">{{ $x.name }}</div>
<div id="enum_{{$x.name}}_details" style="display: none">

<table class="field-table">
Expand Down Expand Up @@ -146,16 +196,20 @@ <h3 style="padding-right: 0">
{{ end }}
</div>
{{ end }}
<div onclick="showhide(document.getElementById('{{$x.name}}_details'))">
<div onclick="toggle(this)">
<span class="inline-type">{{replace (index $x.result 0) "->" "→"}}</span>
<span class="field-name">{{$x.name}}</span>
{{ $ptypes := slice }}
{{ range $x.params }}
{{ $ptypes = $ptypes | append (replace .type "->" "→") }}
{{ end }}
<span class="inline-params">({{ delimit $ptypes ", " }})</span>
{{ $wrappedTypes := slice }}
{{ range $ptypes }}
{{ $wrappedTypes = $wrappedTypes | append (safeHTML (printf "<span class=\"inline-type\" style=\"margin: 0;\">%s</span>" .)) }}
{{ end }}
<span class="inline-params">({{ delimit $wrappedTypes ", " | safeHTML }})</span>
</div>
<div id="{{$x.name}}_details" style="display: none">
<div id="{{$x.name}}_details" class="details" style="display: none">
<div class="field-description">
{{ $x.description | htmlEscape }}
</div>
Expand Down Expand Up @@ -237,3 +291,8 @@ <h3>Changes</h3>
{{- /* Finished generating the release page content */}}

{{ end }}

<script>
for (let x of document.querySelectorAll('.inline-type'))
x.innerHTML = renderType(x.innerHTML);
</script>

0 comments on commit df42cde

Please sign in to comment.