Skip to content

Commit

Permalink
Add internal links to XenAPI reference
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.

Signed-off-by: Colin James <[email protected]>
  • Loading branch information
contificate committed Feb 21, 2025
1 parent 1131ecb commit 8583c5e
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 8583c5e

Please sign in to comment.