Skip to content

Commit

Permalink
Frontend (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
Equinox- authored Jun 4, 2024
1 parent bd325d9 commit a395c98
Show file tree
Hide file tree
Showing 18 changed files with 4,722 additions and 1 deletion.
9 changes: 8 additions & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
name: .NET
on:
push:
paths:
- SteamUtils/**
- SchemaBuilder/**
- patches/**
- .github/workflows/dotnet.yml
workflow_dispatch: {}

on: [push]
jobs:
build:
runs-on: windows-2019
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Frontend
on:
push:
paths:
- SchemaFrontend/**
- .github/workflows/frontend.yml

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # For federated auth
defaults:
run:
working-directory: SchemaFrontend/
steps:
- uses: actions/checkout@v3
- name: Set Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20.x
- run: corepack enable
- run: yarn set version stable
- name: Setup Node.js cache
uses: actions/setup-node@v3
with:
node-version: 20.x
cache: yarn
cache-dependency-path: 'SchemaFrontend/yarn.lock'
- run: yarn install
- run: yarn run build
# Upload Frontend
- name: Upload Frontend
uses: actions/upload-artifact@v3
with:
name: frontend
path: SchemaFrontend/dist/
# Publish to GCS
- name: Log into gcloud
id: auth
if: (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && github.event_name != 'pull_request'
uses: 'google-github-actions/auth@v0'
with:
workload_identity_provider: 'projects/445692247363/locations/global/workloadIdentityPools/github-actions/providers/github-actions'
service_account: '[email protected]'
- name: Release Frontend
uses: 'google-github-actions/upload-cloud-storage@v1'
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
with:
path: SchemaFrontend/dist/
destination: "unofficial-keen-schemas/frontend/"
parent: false
headers: |-
cache-control: public, max-age=300, must-revalidate
10 changes: 10 additions & 0 deletions SchemaFrontend/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true

[*.{js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2
5 changes: 5 additions & 0 deletions SchemaFrontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
.yarn/
.vscode/
.pnp*
22 changes: 22 additions & 0 deletions SchemaFrontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>ACE in Action</title>
<style type="text/css" media="screen">
#editor {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
</style>
</head>
<body>
<div id="editor">function foo(items) {
var x = "All this is syntax highlighted";
return x;
}</div>
<script src="/ace-builds/src-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions SchemaFrontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "keen-unofficial-schema-frontend",
"packageManager": "[email protected]",
"private": true,
"scripts": {
"start": "webpack serve --mode=development",
"build": "webpack --mode=production"
},
"devDependencies": {
"html-webpack-plugin": "^5.6.0",
"ts-loader": "^9.5.1",
"typescript": "^5.4.5",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
},
"dependencies": {
"ace-code": "^1.34.2"
}
}
20 changes: 20 additions & 0 deletions SchemaFrontend/src/desc.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
declare module "ace-code/src/theme/monokai" {
export const cssClass: string;
}

declare module "ace-code/src/tooltip" {
import { Ace } from "ace-code";

export interface AceMouseEvent {
getDocumentPosition(): Ace.Position;
}

export class HoverTooltip {
constructor(parentNode: HTMLElement);
addToEditor(editor: Ace.Editor): void;
removeFromEditor(editor: Ace.Editor): void;
setDataProvider(value: (e: AceMouseEvent, editor: Ace.Editor) => void): void;
showForRange(editor: Ace.Editor, range: Ace.Range, domNode: HTMLElement, startingEvent: AceMouseEvent): void;
hide(startingEvent: AceMouseEvent): void;
}
}
31 changes: 31 additions & 0 deletions SchemaFrontend/src/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as ace from "ace-code";
import { AceTooltips } from "./tooltip";
import { XmlBuilder } from "./xml";
import { SchemaIr } from "./ir";
import { generateExample } from "./generate";

export function setupEditor(editorDom: HTMLElement, schema: string, path: string[]) {
const editor = ace.edit(editorDom);
editor.setReadOnly(true);
const tooltips = new AceTooltips(editor);

import('ace-code/src/mode/xml').then(xml => editor.session.setMode(new xml.Mode()));
import('ace-code/src/theme/monokai').then(theme => {
editor.setStyle(theme.cssClass);
const styleRef = document.getElementById(theme.cssClass);
// Move to end so the theme takes priority.
styleRef.parentElement.appendChild(styleRef);
});

const schemaPath = "https://storage.googleapis.com/unofficial-keen-schemas/latest/" + schema;
fetch(schemaPath + ".json")
.then(response => response.json())
.then(json => json as SchemaIr)
.then(ir => {
const builder = new XmlBuilder({ editor, tooltips, schema: schemaPath + ".xsd" });
generateExample(ir, builder, path);
})
.catch(err => {
console.warn("Failed to load schema IR for " + schema[0], err);
});
}
160 changes: 160 additions & 0 deletions SchemaFrontend/src/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { ItemTypeRef, ObjectType, PrimitiveType, SchemaIr, Type, TypeRef } from "./ir";
import { XmlBuilder } from "./xml";

function typeByName(ir: SchemaIr, name: string): Type {
const type = ir.types[name];
if (type == null)
throw new Error('Type is not in the schema ' + name);
return type;
}

function constrainedTypeByName<T extends Type['$type']>(ir: SchemaIr, name: string, ...constraints: T[]): Type & { $type: T } {
const type = typeByName(ir, name);
if (constraints.indexOf(type.$type as T) == -1)
throw new Error('Type ' + name + ' must be one of [' + constraints.join(', ') + '] but was ' + type.$type);
return type as any;
}

export function generateExample(ir: SchemaIr, builder: XmlBuilder, path: string[]) {
let type: ObjectType = { $type: 'object', elements: ir.rootElements, attributes: {} };
let openedElements = 0;
for (const element of path) {
const [name, customTypeName] = element.split('@', 2);
const property = type.elements[name];
if (property == null) {
throw new Error('Navigating path ' + path.join(' -> ') + ' at ' + element + ', element is missing');
}
builder.startElement(name, property.documentation);
openedElements++;

let itemType: ItemTypeRef;
switch (property.type.$type) {
case "array":
itemType = property.type.item;
if (property.type.wrapperElement != null) {
builder.startElement(property.type.wrapperElement);
openedElements++;
}
break;
case "optional":
itemType = property.type.item;
break;
case 'custom':
case 'primitive':
itemType = property.type;
break;
}

if (customTypeName != null) {
builder.writeAttribute('xsi:type', customTypeName);
type = constrainedTypeByName(ir, customTypeName, 'object');
continue;
}

if (itemType.$type != 'custom') {
throw new Error('Navigating path ' + path.join(' -> ') + ' at ' + element + ', element is not an object');
}
type = constrainedTypeByName(ir, itemType.name, 'object');
}
generateObjectContents(ir, builder, type);
for (let i = 0; i < openedElements; i++) {
builder.closeElement();
}
}

const SampleOmit: string = '__omit__';

function generateObjectContents(ir: SchemaIr, builder: XmlBuilder, type: ObjectType) {
for (const [name, attr] of Object.entries(type.attributes)) {
if (attr.sample == SampleOmit)
continue;
builder.writeAttribute(name, attr.sample ?? attr.default ?? generateAttributeContents(ir, attr.type), attr.documentation);
}
for (const [name, element] of Object.entries(type.elements)) {
if (element.sample == SampleOmit)
continue;
builder.startElement(name, element.documentation);
if (element.sample != null) {
builder.writeContent(element.sample);
}
else if (element.default != null) {
builder.writeContent(element.default);
} else {
generateTypeRefContents(ir, builder, element.type);
}
builder.closeElement();
}
}

function generatePrimitiveContents(type: PrimitiveType): string {
switch (type) {
case "String":
return 'abc';
case "Boolean":
return 'true';
case "Double":
return '123.456';
case "Integer":
return '123';
}
}

function generateAttributeContents(ir: SchemaIr, typeRef: TypeRef): string {
let itemType: ItemTypeRef;
switch (typeRef.$type) {
case "array":
case "optional":
itemType = typeRef.item;
break;
case 'custom':
case 'primitive':
itemType = typeRef;
break;
}
switch (itemType.$type) {
case "primitive":
return generatePrimitiveContents(itemType.type);
case "custom":
const referenced = constrainedTypeByName(ir, itemType.name, 'pattern', 'enum');
switch (referenced.$type) {
case "pattern":
return generateAttributeContents(ir, referenced.type);
case "enum":
const values = Object.keys(referenced.items);
return referenced.flags && values.length >= 2 ? values[0] + ' ' + values[1] : values[0];
}
}
}

function generateTypeRefContents(ir: SchemaIr, builder: XmlBuilder, typeRef: TypeRef) {
let openedElements = 0;
let itemType: ItemTypeRef;
switch (typeRef.$type) {
case "array":
itemType = typeRef.item;
if (typeRef.wrapperElement != null) {
builder.startElement(typeRef.wrapperElement);
openedElements++;
}
break;
case "optional":
itemType = typeRef.item;
break;
case 'custom':
case 'primitive':
itemType = typeRef;
break;
}
switch (itemType.$type) {
case "primitive":
builder.writeContent(generatePrimitiveContents(itemType.type));
break;
case "custom":
generateObjectContents(ir, builder, constrainedTypeByName(ir, itemType.name, 'object'));
break;
}

for (let i = 0; i < openedElements; i++) {
builder.closeElement();
}
}
22 changes: 22 additions & 0 deletions SchemaFrontend/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { locationParameters } from "./util";

(function () {
const editorDom = document.createElement("editor");
document.body.appendChild(editorDom);
editorDom.style.position = 'absolute';
editorDom.style.left = '0';
editorDom.style.right = '0';
editorDom.style.top = '0';
editorDom.style.bottom = '0';

const { schema, path } = locationParameters();
if (schema?.length != 1 || !(path?.length >= 1)) {
editorDom.innerText = 'Schema and path must be defined.';
return;
}
const root = path[path.length - 1].split('@', 2);
const stripPrefix = "MyObjectBuilder_";
document.title = root.length == 2 ? root[1].startsWith(stripPrefix) ? root[1].substring(stripPrefix.length) : root[1] : root[0];

import('./editor').then(editor => editor.setupEditor(editorDom, schema[0], path));
})();
Loading

0 comments on commit a395c98

Please sign in to comment.