initial commit
This commit is contained in:
commit
7a6efbcf90
15 changed files with 1156 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
12
index.html
Normal file
12
index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module">
|
||||||
|
import {main} from "./src/main.js";
|
||||||
|
main();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
package.json
Normal file
6
package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"finalhandler": "^1.1.2",
|
||||||
|
"serve-static": "^1.14.1"
|
||||||
|
}
|
||||||
|
}
|
43
scripts/serve-local.js
Normal file
43
scripts/serve-local.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const finalhandler = require('finalhandler')
|
||||||
|
const http = require('http')
|
||||||
|
const serveStatic = require('serve-static')
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Serve up parent directory with cache disabled
|
||||||
|
const serve = serveStatic(
|
||||||
|
path.resolve(__dirname, "../"),
|
||||||
|
{
|
||||||
|
etag: false,
|
||||||
|
setHeaders: res => {
|
||||||
|
res.setHeader("Pragma", "no-cache");
|
||||||
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
res.setHeader("Expires", "Wed, 21 Oct 2015 07:28:00 GMT");
|
||||||
|
},
|
||||||
|
index: ['index.html', 'index.htm']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create server
|
||||||
|
const server = http.createServer(function onRequest (req, res) {
|
||||||
|
console.log(req.method, req.url);
|
||||||
|
serve(req, res, finalhandler(req, res))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen
|
||||||
|
server.listen(3000);
|
151
src/Link.js
Normal file
151
src/Link.js
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import {createEnum} from "./utils/enum.js";
|
||||||
|
|
||||||
|
const ROOMALIAS_PATTERN = /^#([^:]*):(.+)$/;
|
||||||
|
const ROOMID_PATTERN = /^!([^:]*):(.+)$/;
|
||||||
|
const EVENT_WITH_ROOMID_PATTERN = /^[!]([^:]*):(.+)\/\$([^:]+):(.+)$/;
|
||||||
|
const EVENT_WITH_ROOMALIAS_PATTERN = /^[#]([^:]*):(.+)\/\$([^:]+):(.+)$/;
|
||||||
|
const USERID_PATTERN = /^@([^:]+):(.+)$/;
|
||||||
|
const GROUPID_PATTERN = /^\+([^:]+):(.+)$/;
|
||||||
|
|
||||||
|
export const IdentifierKind = createEnum(
|
||||||
|
"RoomId",
|
||||||
|
"RoomAlias",
|
||||||
|
"UserId",
|
||||||
|
"GroupId",
|
||||||
|
);
|
||||||
|
|
||||||
|
function asPrefix(identifierKind) {
|
||||||
|
switch (identifierKind) {
|
||||||
|
case IdentifierKind.RoomId: return "!";
|
||||||
|
case IdentifierKind.RoomAlias: return "#";
|
||||||
|
case IdentifierKind.GroupId: return "+";
|
||||||
|
case IdentifierKind.UserId: return "@";
|
||||||
|
default: throw new Error("invalid id kind " + identifierKind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkKind = createEnum(
|
||||||
|
"Room",
|
||||||
|
"User",
|
||||||
|
"Group",
|
||||||
|
"Event"
|
||||||
|
)
|
||||||
|
|
||||||
|
function orderedUnique(array) {
|
||||||
|
const copy = [];
|
||||||
|
for (let i = 0; i < array.length; ++i) {
|
||||||
|
if (i === 0 || array.lastIndexOf(array[i], i - 1) === -1) {
|
||||||
|
copy.push(array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Link {
|
||||||
|
static parseFragment(fragment) {
|
||||||
|
let [identifier, queryParams] = fragment.split("?");
|
||||||
|
|
||||||
|
let viaServers = [];
|
||||||
|
if (queryParams) {
|
||||||
|
viaServers = queryParams.split("&")
|
||||||
|
.map(pair => pair.split("="))
|
||||||
|
.filter(([key, value]) => key === "via")
|
||||||
|
.map(([,value]) => value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identifier.startsWith("#/")) {
|
||||||
|
identifier = identifier.substr(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind;
|
||||||
|
let matches;
|
||||||
|
// longest first, so they dont get caught by ROOMALIAS_PATTERN and ROOMID_PATTERN
|
||||||
|
matches = EVENT_WITH_ROOMID_PATTERN.exec(identifier);
|
||||||
|
if (matches) {
|
||||||
|
const roomServer = matches[2];
|
||||||
|
const messageServer = matches[4];
|
||||||
|
const roomLocalPart = matches[1];
|
||||||
|
const messageLocalPart = matches[3];
|
||||||
|
return new Link(viaServers, IdentifierKind.RoomId, roomLocalPart, roomServer, messageLocalPart, messageServer);
|
||||||
|
}
|
||||||
|
matches = EVENT_WITH_ROOMALIAS_PATTERN.exec(identifier);
|
||||||
|
if (matches) {
|
||||||
|
const roomServer = matches[2];
|
||||||
|
const messageServer = matches[4];
|
||||||
|
const roomLocalPart = matches[1];
|
||||||
|
const messageLocalPart = matches[3];
|
||||||
|
return new Link(viaServers, IdentifierKind.RoomAlias, roomLocalPart, roomServer, messageLocalPart, messageServer);
|
||||||
|
}
|
||||||
|
matches = USERID_PATTERN.exec(identifier);
|
||||||
|
if (matches) {
|
||||||
|
const server = matches[2];
|
||||||
|
const localPart = matches[1];
|
||||||
|
return new Link(viaServers, IdentifierKind.UserId, localPart, server);
|
||||||
|
}
|
||||||
|
matches = ROOMALIAS_PATTERN.exec(identifier);
|
||||||
|
if (matches) {
|
||||||
|
const server = matches[2];
|
||||||
|
const localPart = matches[1];
|
||||||
|
return new Link(viaServers, IdentifierKind.RoomAlias, localPart, server);
|
||||||
|
}
|
||||||
|
matches = ROOMID_PATTERN.exec(identifier);
|
||||||
|
if (matches) {
|
||||||
|
const server = matches[2];
|
||||||
|
const localPart = matches[1];
|
||||||
|
return new Link(viaServers, IdentifierKind.RoomId, localPart, server);
|
||||||
|
}
|
||||||
|
matches = GROUPID_PATTERN.exec(identifier);
|
||||||
|
if (matches) {
|
||||||
|
const server = matches[2];
|
||||||
|
const localPart = matches[1];
|
||||||
|
return new Link(viaServers, IdentifierKind.GroupId, localPart, server);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(viaServers, identifierKind, localPart, server, messageLocalPart = null, messageServer = null) {
|
||||||
|
const servers = [server];
|
||||||
|
if (messageServer) {
|
||||||
|
servers.push(messageServer);
|
||||||
|
}
|
||||||
|
servers.push(...viaServers);
|
||||||
|
this.servers = orderedUnique(servers);
|
||||||
|
this.identifierKind = identifierKind;
|
||||||
|
this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`;
|
||||||
|
this.eventId = messageLocalPart ? `$${messageLocalPart}:${messageServer}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get kind() {
|
||||||
|
if (this.eventId) {
|
||||||
|
return LinkKind.Event;
|
||||||
|
}
|
||||||
|
switch (this.identifierKind) {
|
||||||
|
case IdentifierKind.RoomId:
|
||||||
|
case IdentifierKind.RoomAlias:
|
||||||
|
return LinkKind.Room;
|
||||||
|
case IdentifierKind.UserId:
|
||||||
|
return LinkKind.User;
|
||||||
|
case IdentifierKind.GroupId:
|
||||||
|
return LinkKind.Group;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
345
src/TemplateView.js
Normal file
345
src/TemplateView.js
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS } from "./html.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
Bindable template. Renders once, and allows bindings for given nodes. If you need
|
||||||
|
to change the structure on a condition, use a subtemplate (if)
|
||||||
|
|
||||||
|
supports
|
||||||
|
- event handlers (attribute fn value with name that starts with on)
|
||||||
|
- one way binding of attributes (other attribute fn value)
|
||||||
|
- one way binding of text values (child fn value)
|
||||||
|
- refs to get dom nodes
|
||||||
|
- className binding returning object with className => enabled map
|
||||||
|
- add subviews inside the template
|
||||||
|
*/
|
||||||
|
// TODO: should we rename this to BoundView or something? As opposed to StaticView ...
|
||||||
|
export class TemplateView {
|
||||||
|
constructor(value, render = undefined) {
|
||||||
|
this._value = value;
|
||||||
|
this._render = render;
|
||||||
|
this._eventListeners = null;
|
||||||
|
this._bindings = null;
|
||||||
|
this._subViews = null;
|
||||||
|
this._root = null;
|
||||||
|
this._boundUpdateFromValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_subscribe() {
|
||||||
|
if (typeof this._value?.on === "function") {
|
||||||
|
this._boundUpdateFromValue = this._updateFromValue.bind(this);
|
||||||
|
this._value.on("change", this._boundUpdateFromValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_unsubscribe() {
|
||||||
|
if (this._boundUpdateFromValue) {
|
||||||
|
if (typeof this._value.off === "function") {
|
||||||
|
this._value.off("change", this._boundUpdateFromValue);
|
||||||
|
}
|
||||||
|
this._boundUpdateFromValue = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_attach() {
|
||||||
|
if (this._eventListeners) {
|
||||||
|
for (let {node, name, fn, useCapture} of this._eventListeners) {
|
||||||
|
node.addEventListener(name, fn, useCapture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_detach() {
|
||||||
|
if (this._eventListeners) {
|
||||||
|
for (let {node, name, fn, useCapture} of this._eventListeners) {
|
||||||
|
node.removeEventListener(name, fn, useCapture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(options) {
|
||||||
|
const builder = new TemplateBuilder(this);
|
||||||
|
if (this._render) {
|
||||||
|
this._root = this._render(builder, this._value);
|
||||||
|
} else if (this.render) { // overriden in subclass
|
||||||
|
this._root = this.render(builder, this._value);
|
||||||
|
} else {
|
||||||
|
throw new Error("no render function passed in, or overriden in subclass");
|
||||||
|
}
|
||||||
|
const parentProvidesUpdates = options && options.parentProvidesUpdates;
|
||||||
|
if (!parentProvidesUpdates) {
|
||||||
|
this._subscribe();
|
||||||
|
}
|
||||||
|
this._attach();
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
this._detach();
|
||||||
|
this._unsubscribe();
|
||||||
|
if (this._subViews) {
|
||||||
|
for (const v of this._subViews) {
|
||||||
|
v.unmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root() {
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(value) {
|
||||||
|
this._value = value;
|
||||||
|
if (this._bindings) {
|
||||||
|
for (const binding of this._bindings) {
|
||||||
|
binding();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateFromValue(changedProps) {
|
||||||
|
this.update(this._value, changedProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
_addEventListener(node, name, fn, useCapture = false) {
|
||||||
|
if (!this._eventListeners) {
|
||||||
|
this._eventListeners = [];
|
||||||
|
}
|
||||||
|
this._eventListeners.push({node, name, fn, useCapture});
|
||||||
|
}
|
||||||
|
|
||||||
|
_addBinding(bindingFn) {
|
||||||
|
if (!this._bindings) {
|
||||||
|
this._bindings = [];
|
||||||
|
}
|
||||||
|
this._bindings.push(bindingFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
_addSubView(view) {
|
||||||
|
if (!this._subViews) {
|
||||||
|
this._subViews = [];
|
||||||
|
}
|
||||||
|
this._subViews.push(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// what is passed to render
|
||||||
|
class TemplateBuilder {
|
||||||
|
constructor(templateView) {
|
||||||
|
this._templateView = templateView;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _value() {
|
||||||
|
return this._templateView._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(node, name, fn, useCapture = false) {
|
||||||
|
this._templateView._addEventListener(node, name, fn, useCapture);
|
||||||
|
}
|
||||||
|
|
||||||
|
_addAttributeBinding(node, name, fn) {
|
||||||
|
let prevValue = undefined;
|
||||||
|
const binding = () => {
|
||||||
|
const newValue = fn(this._value);
|
||||||
|
if (prevValue !== newValue) {
|
||||||
|
prevValue = newValue;
|
||||||
|
setAttribute(node, name, newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._templateView._addBinding(binding);
|
||||||
|
binding();
|
||||||
|
}
|
||||||
|
|
||||||
|
_addClassNamesBinding(node, obj) {
|
||||||
|
this._addAttributeBinding(node, "className", value => classNames(obj, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
_addTextBinding(fn) {
|
||||||
|
const initialValue = fn(this._value);
|
||||||
|
const node = text(initialValue);
|
||||||
|
let prevValue = initialValue;
|
||||||
|
const binding = () => {
|
||||||
|
const newValue = fn(this._value);
|
||||||
|
if (prevValue !== newValue) {
|
||||||
|
prevValue = newValue;
|
||||||
|
node.textContent = newValue+"";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._templateView._addBinding(binding);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setNodeAttributes(node, attributes) {
|
||||||
|
for(let [key, value] of Object.entries(attributes)) {
|
||||||
|
const isFn = typeof value === "function";
|
||||||
|
// binding for className as object of className => enabled
|
||||||
|
if (key === "className" && typeof value === "object" && value !== null) {
|
||||||
|
if (objHasFns(value)) {
|
||||||
|
this._addClassNamesBinding(node, value);
|
||||||
|
} else {
|
||||||
|
setAttribute(node, key, classNames(value));
|
||||||
|
}
|
||||||
|
} else if (key.startsWith("on") && key.length > 2 && isFn) {
|
||||||
|
const eventName = key.substr(2, 1).toLowerCase() + key.substr(3);
|
||||||
|
const handler = value;
|
||||||
|
this._templateView._addEventListener(node, eventName, handler);
|
||||||
|
} else if (isFn) {
|
||||||
|
this._addAttributeBinding(node, key, value);
|
||||||
|
} else {
|
||||||
|
setAttribute(node, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setNodeChildren(node, children) {
|
||||||
|
if (!Array.isArray(children)) {
|
||||||
|
children = [children];
|
||||||
|
}
|
||||||
|
for (let child of children) {
|
||||||
|
if (typeof child === "function") {
|
||||||
|
child = this._addTextBinding(child);
|
||||||
|
} else if (!child.nodeType) {
|
||||||
|
// not a DOM node, turn into text
|
||||||
|
child = text(child);
|
||||||
|
}
|
||||||
|
node.appendChild(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_addReplaceNodeBinding(fn, renderNode) {
|
||||||
|
let prevValue = fn(this._value);
|
||||||
|
let node = renderNode(null);
|
||||||
|
|
||||||
|
const binding = () => {
|
||||||
|
const newValue = fn(this._value);
|
||||||
|
if (prevValue !== newValue) {
|
||||||
|
prevValue = newValue;
|
||||||
|
const newNode = renderNode(node);
|
||||||
|
if (node.parentNode) {
|
||||||
|
node.parentNode.replaceChild(newNode, node);
|
||||||
|
}
|
||||||
|
node = newNode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._templateView._addBinding(binding);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
el(name, attributes, children) {
|
||||||
|
return this.elNS(HTML_NS, name, attributes, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
elNS(ns, name, attributes, children) {
|
||||||
|
if (attributes && isChildren(attributes)) {
|
||||||
|
children = attributes;
|
||||||
|
attributes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = document.createElementNS(ns, name);
|
||||||
|
|
||||||
|
if (attributes) {
|
||||||
|
this._setNodeAttributes(node, attributes);
|
||||||
|
}
|
||||||
|
if (children) {
|
||||||
|
this._setNodeChildren(node, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this insert a view, and is not a view factory for `if`, so returns the root element to insert in the template
|
||||||
|
// you should not call t.view() and not use the result (e.g. attach the result to the template DOM tree).
|
||||||
|
view(view) {
|
||||||
|
let root;
|
||||||
|
try {
|
||||||
|
root = view.mount();
|
||||||
|
} catch (err) {
|
||||||
|
return errorToDOM(err);
|
||||||
|
}
|
||||||
|
this._templateView._addSubView(view);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sugar
|
||||||
|
createTemplate(render) {
|
||||||
|
return vm => new TemplateView(vm, render);
|
||||||
|
}
|
||||||
|
|
||||||
|
// map a value to a view, every time the value changes
|
||||||
|
mapView(mapFn, viewCreator) {
|
||||||
|
return this._addReplaceNodeBinding(mapFn, (prevNode) => {
|
||||||
|
if (prevNode && prevNode.nodeType !== Node.COMMENT_NODE) {
|
||||||
|
const subViews = this._templateView._subViews;
|
||||||
|
const viewIdx = subViews.findIndex(v => v.root() === prevNode);
|
||||||
|
if (viewIdx !== -1) {
|
||||||
|
const [view] = subViews.splice(viewIdx, 1);
|
||||||
|
view.unmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const view = viewCreator(mapFn(this._value));
|
||||||
|
if (view) {
|
||||||
|
return this.view(view);
|
||||||
|
} else {
|
||||||
|
return document.createComment("node binding placeholder");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a conditional subtemplate
|
||||||
|
if(fn, viewCreator) {
|
||||||
|
return this.mapView(
|
||||||
|
value => !!fn(value),
|
||||||
|
enabled => enabled ? viewCreator(this._value) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function errorToDOM(error) {
|
||||||
|
const stack = new Error().stack;
|
||||||
|
const callee = stack.split("\n")[1];
|
||||||
|
return tag.div([
|
||||||
|
tag.h2("Something went wrong…"),
|
||||||
|
tag.h3(error.message),
|
||||||
|
tag.p(`This occurred while running ${callee}.`),
|
||||||
|
tag.pre(error.stack),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function objHasFns(obj) {
|
||||||
|
for(const value of Object.values(obj)) {
|
||||||
|
if (typeof value === "function") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [ns, tags] of Object.entries(TAG_NAMES)) {
|
||||||
|
for (const tag of tags) {
|
||||||
|
TemplateBuilder.prototype[tag] = function(attributes, children) {
|
||||||
|
return this.elNS(ns, tag, attributes, children);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
59
src/ViewModel.js
Normal file
59
src/ViewModel.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
this._handlersByName = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(name, ...values) {
|
||||||
|
const handlers = this._handlersByName[name];
|
||||||
|
if (handlers) {
|
||||||
|
for(const h of handlers) {
|
||||||
|
h(...values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on(name, callback) {
|
||||||
|
let handlers = this._handlersByName[name];
|
||||||
|
if (!handlers) {
|
||||||
|
this.onFirstSubscriptionAdded(name);
|
||||||
|
this._handlersByName[name] = handlers = new Set();
|
||||||
|
}
|
||||||
|
handlers.add(callback);
|
||||||
|
return () => {
|
||||||
|
this.off(name, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
off(name, callback) {
|
||||||
|
const handlers = this._handlersByName[name];
|
||||||
|
if (handlers) {
|
||||||
|
handlers.delete(callback);
|
||||||
|
if (handlers.length === 0) {
|
||||||
|
delete this._handlersByName[name];
|
||||||
|
this.onLastSubscriptionRemoved(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ViewModel extends EventEmitter {
|
||||||
|
emitChange() {
|
||||||
|
this.emit("change");
|
||||||
|
}
|
||||||
|
}
|
26
src/create/CreateLinkViewModel.js
Normal file
26
src/create/CreateLinkViewModel.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class CreateLinkViewModel extends ViewModel {
|
||||||
|
createLink(identifier) {
|
||||||
|
this._link = Link.fromIdentifier(identifier);
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
get link() {
|
||||||
|
this._link.toURL();
|
||||||
|
}
|
||||||
|
}
|
111
src/html.js
Normal file
111
src/html.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// DOM helper functions
|
||||||
|
|
||||||
|
export function isChildren(children) {
|
||||||
|
// children should be an not-object (that's the attributes), or a domnode, or an array
|
||||||
|
return typeof children !== "object" || !!children.nodeType || Array.isArray(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function classNames(obj, value) {
|
||||||
|
return Object.entries(obj).reduce((cn, [name, enabled]) => {
|
||||||
|
if (typeof enabled === "function") {
|
||||||
|
enabled = enabled(value);
|
||||||
|
}
|
||||||
|
if (enabled) {
|
||||||
|
return cn + (cn.length ? " " : "") + name;
|
||||||
|
} else {
|
||||||
|
return cn;
|
||||||
|
}
|
||||||
|
}, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAttribute(el, name, value) {
|
||||||
|
if (name === "className") {
|
||||||
|
name = "class";
|
||||||
|
}
|
||||||
|
if (value === false) {
|
||||||
|
el.removeAttribute(name);
|
||||||
|
} else {
|
||||||
|
if (value === true) {
|
||||||
|
value = name;
|
||||||
|
}
|
||||||
|
el.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function el(elementName, attributes, children) {
|
||||||
|
return elNS(HTML_NS, elementName, attributes, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function elNS(ns, elementName, attributes, children) {
|
||||||
|
if (attributes && isChildren(attributes)) {
|
||||||
|
children = attributes;
|
||||||
|
attributes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const e = document.createElementNS(ns, elementName);
|
||||||
|
|
||||||
|
if (attributes) {
|
||||||
|
for (let [name, value] of Object.entries(attributes)) {
|
||||||
|
if (name === "className" && typeof value === "object" && value !== null) {
|
||||||
|
value = classNames(value);
|
||||||
|
}
|
||||||
|
setAttribute(e, name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children) {
|
||||||
|
if (!Array.isArray(children)) {
|
||||||
|
children = [children];
|
||||||
|
}
|
||||||
|
for (let c of children) {
|
||||||
|
if (!c.nodeType) {
|
||||||
|
c = text(c);
|
||||||
|
}
|
||||||
|
e.appendChild(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function text(str) {
|
||||||
|
return document.createTextNode(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||||
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
export const TAG_NAMES = {
|
||||||
|
[HTML_NS]: [
|
||||||
|
"br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
|
"p", "strong", "em", "span", "img", "section", "main", "article", "aside",
|
||||||
|
"pre", "button", "time", "input", "textarea", "label", "form", "progress", "output"],
|
||||||
|
[SVG_NS]: ["svg", "circle"]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tag = {};
|
||||||
|
|
||||||
|
|
||||||
|
for (const [ns, tags] of Object.entries(TAG_NAMES)) {
|
||||||
|
for (const tagName of tags) {
|
||||||
|
tag[tagName] = function(attributes, children) {
|
||||||
|
return elNS(ns, tagName, attributes, children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/main.js
Normal file
16
src/main.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {xhrRequest} from "./utils/xhr.js";
|
||||||
|
import {validateHomeServer} from "./matrix/HomeServer.js";
|
||||||
|
import {Link, LinkKind} from "./Link.js";
|
||||||
|
|
||||||
|
export async function main() {
|
||||||
|
const link = Link.parseFragment(location.hash);
|
||||||
|
if (!link) {
|
||||||
|
throw new Error("bad link");
|
||||||
|
}
|
||||||
|
const hs = await validateHomeServer(xhrRequest, link.servers[0]);
|
||||||
|
if (link.kind === LinkKind.User) {
|
||||||
|
const profile = await hs.getUserProfile(link.identifier);
|
||||||
|
const imageURL = hs.mxcUrlThumbnail(profile.avatar_url, 64, 64, "crop");
|
||||||
|
console.log(imageURL);
|
||||||
|
}
|
||||||
|
}
|
76
src/matrix/HomeServer.js
Normal file
76
src/matrix/HomeServer.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function validateHomeServer(request, baseURL) {
|
||||||
|
if (!baseURL.startsWith("http://") && !baseURL.startsWith("https://")) {
|
||||||
|
baseURL = `https://${baseURL}`;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const {status, body} = await request(`${baseURL}/.well-known/matrix/client`, {method: "GET"}).response();
|
||||||
|
if (status === 200) {
|
||||||
|
const proposedBaseURL = body?.['m.homeserver']?.base_url;
|
||||||
|
if (typeof proposedBaseURL === "string") {
|
||||||
|
baseURL = proposedBaseURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const {status} = await request(`${baseURL}/_matrix/client/versions`, {method: "GET"}).response();
|
||||||
|
if (status !== 200) {
|
||||||
|
throw new Error(`Invalid versions response from ${baseURL}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new HomeServer(request, baseURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HomeServer {
|
||||||
|
constructor(request, baseURL) {
|
||||||
|
this._request = request;
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserProfile(userId) {
|
||||||
|
const {body} = await this._request(`${this.baseURL}/_matrix/client/r0/profile/${userId}`, {method: "GET"}).response();
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroupProfile(groupId) {
|
||||||
|
//`/_matrix/client/r0/groups/${groupId}/profile`
|
||||||
|
}
|
||||||
|
|
||||||
|
getPublicRooms() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mxcUrlThumbnail(url, width, height, method) {
|
||||||
|
const parts = parseMxcUrl(url);
|
||||||
|
if (parts) {
|
||||||
|
const [serverName, mediaId] = parts;
|
||||||
|
const httpUrl = `${this.baseURL}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
|
||||||
|
return httpUrl + `?width=${width}&height=${height}&method=${method}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMxcUrl(url) {
|
||||||
|
const prefix = "mxc://";
|
||||||
|
if (url.startsWith(prefix)) {
|
||||||
|
return url.substr(prefix.length).split("/", 2);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
26
src/utils/enum.js
Normal file
26
src/utils/enum.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function createEnum(...values) {
|
||||||
|
const obj = {};
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error("Invalid enum value name" + value?.toString());
|
||||||
|
}
|
||||||
|
obj[value] = value;
|
||||||
|
}
|
||||||
|
return Object.freeze(obj);
|
||||||
|
}
|
32
src/utils/error.js
Normal file
32
src/utils/error.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ConnectionError extends Error {
|
||||||
|
constructor(message, isTimeout) {
|
||||||
|
super(message || "ConnectionError");
|
||||||
|
this.isTimeout = isTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return "ConnectionError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AbortError extends Error {
|
||||||
|
get name() {
|
||||||
|
return "AbortError";
|
||||||
|
}
|
||||||
|
}
|
97
src/utils/xhr.js
Normal file
97
src/utils/xhr.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbortError,
|
||||||
|
ConnectionError
|
||||||
|
} from "./error.js";
|
||||||
|
|
||||||
|
function addCacheBuster(urlStr, random = Math.random) {
|
||||||
|
// XHR doesn't have a good way to disable cache,
|
||||||
|
// so add a random query param
|
||||||
|
// see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
|
||||||
|
if (urlStr.includes("?")) {
|
||||||
|
urlStr = urlStr + "&";
|
||||||
|
} else {
|
||||||
|
urlStr = urlStr + "?";
|
||||||
|
}
|
||||||
|
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestResult {
|
||||||
|
constructor(promise, xhr) {
|
||||||
|
this._promise = promise;
|
||||||
|
this._xhr = xhr;
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
this._xhr.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
response() {
|
||||||
|
return this._promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createXhr(url, {method, headers, timeout, uploadProgress}) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open(method, url);
|
||||||
|
|
||||||
|
if (headers) {
|
||||||
|
for(const [name, value] of headers.entries()) {
|
||||||
|
try {
|
||||||
|
xhr.setRequestHeader(name, value);
|
||||||
|
} catch (err) {
|
||||||
|
console.info(`Could not set ${name} header: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (timeout) {
|
||||||
|
xhr.timeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProgress) {
|
||||||
|
xhr.upload.addEventListener("progress", evt => uploadProgress(evt.loaded));
|
||||||
|
}
|
||||||
|
|
||||||
|
return xhr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function xhrAsPromise(xhr, method, url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
xhr.addEventListener("load", () => resolve(xhr));
|
||||||
|
xhr.addEventListener("abort", () => reject(new AbortError()));
|
||||||
|
xhr.addEventListener("error", () => reject(new ConnectionError(`Error ${method} ${url}`)));
|
||||||
|
xhr.addEventListener("timeout", () => reject(new ConnectionError(`Timeout ${method} ${url}`, true)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function xhrRequest(url, options) {
|
||||||
|
let {cache, body, method} = options;
|
||||||
|
if (!cache) {
|
||||||
|
url = addCacheBuster(url);
|
||||||
|
}
|
||||||
|
const xhr = createXhr(url, options);
|
||||||
|
const promise = xhrAsPromise(xhr, method, url).then(xhr => {
|
||||||
|
const {status} = xhr;
|
||||||
|
const body = JSON.parse(xhr.responseText);
|
||||||
|
return {status, body};
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.send(body || null);
|
||||||
|
|
||||||
|
return new RequestResult(promise, xhr);
|
||||||
|
}
|
155
yarn.lock
Normal file
155
yarn.lock
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
version "2.6.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||||
|
dependencies:
|
||||||
|
ms "2.0.0"
|
||||||
|
|
||||||
|
depd@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||||
|
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
||||||
|
|
||||||
|
destroy@~1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||||
|
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
|
||||||
|
|
||||||
|
ee-first@1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||||
|
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||||
|
|
||||||
|
encodeurl@~1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||||
|
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||||
|
|
||||||
|
escape-html@~1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||||
|
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||||
|
|
||||||
|
etag@~1.8.1:
|
||||||
|
version "1.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||||
|
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||||
|
|
||||||
|
finalhandler@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
|
||||||
|
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
|
||||||
|
dependencies:
|
||||||
|
debug "2.6.9"
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
on-finished "~2.3.0"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
statuses "~1.5.0"
|
||||||
|
unpipe "~1.0.0"
|
||||||
|
|
||||||
|
fresh@0.5.2:
|
||||||
|
version "0.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||||
|
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
|
||||||
|
|
||||||
|
http-errors@~1.7.2:
|
||||||
|
version "1.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
|
||||||
|
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
|
||||||
|
dependencies:
|
||||||
|
depd "~1.1.2"
|
||||||
|
inherits "2.0.4"
|
||||||
|
setprototypeof "1.1.1"
|
||||||
|
statuses ">= 1.5.0 < 2"
|
||||||
|
toidentifier "1.0.0"
|
||||||
|
|
||||||
|
inherits@2.0.4:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
|
mime@1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
|
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||||
|
|
||||||
|
ms@2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||||
|
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
||||||
|
|
||||||
|
on-finished@~2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||||
|
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
|
||||||
|
dependencies:
|
||||||
|
ee-first "1.1.1"
|
||||||
|
|
||||||
|
parseurl@~1.3.3:
|
||||||
|
version "1.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||||
|
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||||
|
|
||||||
|
range-parser@~1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||||
|
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||||
|
|
||||||
|
send@0.17.1:
|
||||||
|
version "0.17.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
|
||||||
|
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
|
||||||
|
dependencies:
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "~1.1.2"
|
||||||
|
destroy "~1.0.4"
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
etag "~1.8.1"
|
||||||
|
fresh "0.5.2"
|
||||||
|
http-errors "~1.7.2"
|
||||||
|
mime "1.6.0"
|
||||||
|
ms "2.1.1"
|
||||||
|
on-finished "~2.3.0"
|
||||||
|
range-parser "~1.2.1"
|
||||||
|
statuses "~1.5.0"
|
||||||
|
|
||||||
|
serve-static@^1.14.1:
|
||||||
|
version "1.14.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
|
||||||
|
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
|
||||||
|
dependencies:
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
send "0.17.1"
|
||||||
|
|
||||||
|
setprototypeof@1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
||||||
|
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
|
||||||
|
|
||||||
|
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||||
|
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
||||||
|
|
||||||
|
toidentifier@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||||
|
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||||
|
|
||||||
|
unpipe@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
|
Loading…
Reference in a new issue