initial commit
This commit is contained in:
commit
7a6efbcf90
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 New Issue
Block a user