mirror of
https://github.com/tridactyl/tridactyl.git
synced 2026-09-10 07:16:33 -04:00
Re-implement metadata API on top of tsdoc output
This commit is contained in:
parent
77fc67ca0c
commit
2bd1f5502b
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -10,9 +10,8 @@ native/native_main
|
|||
native_main.spec
|
||||
.wine-pyinstaller
|
||||
tags
|
||||
compiler/*.js
|
||||
compiler/**/*.js
|
||||
.*.generated.ts
|
||||
.*.generated.json
|
||||
.tmp/
|
||||
.DS_Store
|
||||
.build_cache/
|
||||
|
|
|
|||
|
|
@ -1,274 +0,0 @@
|
|||
import * as ts from "typescript"
|
||||
import * as fs from "fs"
|
||||
import * as commandLineArgs from "command-line-args"
|
||||
import * as AllTypes from "./types/AllTypes"
|
||||
import * as AllMetadata from "./metadata/AllMetadata"
|
||||
|
||||
export function toSimpleType(typeNode) {
|
||||
switch (typeNode.kind) {
|
||||
case ts.SyntaxKind.VoidKeyword:
|
||||
return new AllTypes.VoidType()
|
||||
// IndexedAccessTypes are things like `fn<T keyof Class>(x: T, y: Class[T])`
|
||||
// This doesn't seem to be easy to deal with so let's kludge it for now
|
||||
case ts.SyntaxKind.IndexedAccessType:
|
||||
// Unknown is just like any, but slightly stricter
|
||||
case ts.SyntaxKind.UnknownKeyword:
|
||||
case ts.SyntaxKind.AnyKeyword:
|
||||
return new AllTypes.AnyType()
|
||||
case ts.SyntaxKind.BooleanKeyword:
|
||||
return new AllTypes.BooleanType()
|
||||
case ts.SyntaxKind.NumberKeyword:
|
||||
return new AllTypes.NumberType()
|
||||
case ts.SyntaxKind.ObjectKeyword:
|
||||
return new AllTypes.ObjectType()
|
||||
case ts.SyntaxKind.StringKeyword:
|
||||
return new AllTypes.StringType()
|
||||
case ts.SyntaxKind.Parameter:
|
||||
let n = toSimpleType(typeNode.type)
|
||||
n.name = typeNode.name.original.escapedText
|
||||
n.isDotDotDot = !!typeNode.dotDotDotToken
|
||||
n.isQuestion = !!typeNode.questionToken
|
||||
return n
|
||||
case ts.SyntaxKind.TypeReference:
|
||||
if (!typeNode.typeArguments) {
|
||||
// If there are no typeArguments, this is not a parametric type and we can return the type directly
|
||||
try {
|
||||
return toSimpleType(
|
||||
typeNode.typeName.symbol.declarations[0].type,
|
||||
)
|
||||
} catch (e) {
|
||||
// Fall back to what you'd do with typeArguments
|
||||
}
|
||||
}
|
||||
let args = typeNode.typeArguments
|
||||
? typeNode.typeArguments.map(t =>
|
||||
toSimpleType(typeNode.typeArguments[0]),
|
||||
)
|
||||
: []
|
||||
return new AllTypes.TypeReferenceType(
|
||||
typeNode.typeName.escapedText,
|
||||
args,
|
||||
)
|
||||
case ts.SyntaxKind.FunctionType:
|
||||
// generics = (typeNode.typeParameters || []).map(p => new AllTypes.SimpleType(p))
|
||||
return new AllTypes.FunctionType(
|
||||
typeNode.parameters.map(p => toSimpleType(p)),
|
||||
toSimpleType(typeNode.type),
|
||||
)
|
||||
case ts.SyntaxKind.TypeLiteral:
|
||||
let members = typeNode.members
|
||||
.map(member => {
|
||||
if (member.kind == ts.SyntaxKind.IndexSignature) {
|
||||
// Something like this: { [str: string]: string[] }
|
||||
return ["", toSimpleType(member.type)]
|
||||
}
|
||||
// Very fun feature: when you have an object literal with >20 members, typescript will decide to replace some of them with a "... X more ..." node that obviously doesn't have a corresponding symbol, hence this check and the filter after the map
|
||||
if (member.name.symbol)
|
||||
return [
|
||||
member.name.symbol.escapedName,
|
||||
toSimpleType(member.type),
|
||||
]
|
||||
})
|
||||
.filter(m => m)
|
||||
return new AllTypes.ObjectType(new Map(members))
|
||||
case ts.SyntaxKind.ArrayType:
|
||||
return new AllTypes.ArrayType(toSimpleType(typeNode.elementType))
|
||||
case ts.SyntaxKind.TupleType:
|
||||
return new AllTypes.TupleType(
|
||||
typeNode.elementTypes.map(t => toSimpleType(t)),
|
||||
)
|
||||
case ts.SyntaxKind.UnionType:
|
||||
return new AllTypes.UnionType(
|
||||
typeNode.types.map(t => toSimpleType(t)),
|
||||
)
|
||||
break
|
||||
case ts.SyntaxKind.LiteralType:
|
||||
return new AllTypes.LiteralTypeType(typeNode.literal.text)
|
||||
break
|
||||
default:
|
||||
console.log(typeNode)
|
||||
throw new Error(`Unhandled kind (${typeNode.kind}) for ${typeNode}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** True if node is visible outside its file, false otherwise */
|
||||
function isNodeExported(node: ts.Node): boolean {
|
||||
return (
|
||||
(ts.getCombinedModifierFlags(<ts.Declaration>node) &
|
||||
ts.ModifierFlags.Export) !==
|
||||
0 ||
|
||||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
|
||||
)
|
||||
}
|
||||
|
||||
/** True if node is marked as @hidden in its documentation */
|
||||
function isNodeHidden(sourceFile, node): boolean {
|
||||
return (
|
||||
sourceFile &&
|
||||
node.jsDoc &&
|
||||
!!node.jsDoc.find(
|
||||
doc =>
|
||||
sourceFile.text.slice(doc.pos, doc.end).search("@hidden") != -1,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function visit(
|
||||
checker: any,
|
||||
sourceFile: any,
|
||||
file: AllMetadata.FileMetadata,
|
||||
node: any,
|
||||
) {
|
||||
let symbol = checker.getSymbolAtLocation(node.name)
|
||||
if (symbol && isNodeExported(node)) {
|
||||
let nodeName = symbol.escapedName
|
||||
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
// Grab the doc, default to empty string
|
||||
let doc =
|
||||
ts.displayPartsToString(symbol.getDocumentationComment()) ||
|
||||
""
|
||||
// Grab the type
|
||||
let ttype = checker.getTypeOfSymbolAtLocation(
|
||||
symbol,
|
||||
symbol.valueDeclaration!,
|
||||
)
|
||||
// If the function has a type, try to convert it, if it doesn't, default to any
|
||||
let t = ttype
|
||||
? toSimpleType(checker.typeToTypeNode(ttype))
|
||||
: new AllTypes.AnyType()
|
||||
file.setFunction(
|
||||
nodeName,
|
||||
new AllMetadata.SymbolMetadata(
|
||||
doc,
|
||||
t,
|
||||
isNodeHidden(sourceFile, node),
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
let clazz = file.getClass(nodeName)
|
||||
if (!clazz) {
|
||||
clazz = new AllMetadata.ClassMetadata()
|
||||
file.setClass(nodeName, clazz)
|
||||
}
|
||||
symbol.members.forEach((sym, name, map) => {
|
||||
// Can't get doc/type from these special functions
|
||||
// Or at least, it requires work that might not be needed for now
|
||||
if (["__constructor", "get", "set"].includes(name)) return
|
||||
|
||||
// Grab the doc, default to empty string
|
||||
let doc =
|
||||
ts.displayPartsToString(
|
||||
sym.getDocumentationComment(),
|
||||
) || ""
|
||||
// Grab the type
|
||||
let ttype = checker.getTypeOfSymbolAtLocation(
|
||||
sym,
|
||||
sym.valueDeclaration!,
|
||||
)
|
||||
// If the function has a type, try to convert it, if it doesn't, default to any
|
||||
let t = ttype
|
||||
? toSimpleType(checker.typeToTypeNode(ttype))
|
||||
: new AllTypes.AnyType()
|
||||
clazz.setMember(
|
||||
name,
|
||||
new AllMetadata.SymbolMetadata(
|
||||
doc,
|
||||
t,
|
||||
isNodeHidden(sourceFile, node),
|
||||
),
|
||||
)
|
||||
})
|
||||
return
|
||||
|
||||
// Other declaration syntaxkinds:
|
||||
// case ts.SyntaxKind.VariableDeclaration:
|
||||
// case ts.SyntaxKind.VariableDeclarationList:
|
||||
// case ts.SyntaxKind.PropertyDeclaration:
|
||||
// case ts.SyntaxKind.MethodDeclaration:
|
||||
// case ts.SyntaxKind.EndOfDeclarationMarker:
|
||||
// case ts.SyntaxKind.MergeDeclarationMarker:
|
||||
// case ts.SyntaxKind.MissingDeclaration:
|
||||
// case ts.SyntaxKind.ClassExpression:
|
||||
// case ts.SyntaxKind.InterfaceDeclaration:
|
||||
// case ts.SyntaxKind.TypeAliasDeclaration:
|
||||
// case ts.SyntaxKind.EnumDeclaration:
|
||||
// case ts.SyntaxKind.ModuleDeclaration:
|
||||
// case ts.SyntaxKind.ImportEqualsDeclaration:
|
||||
// case ts.SyntaxKind.ImportDeclaration:
|
||||
// case ts.SyntaxKind.NamespaceExportDeclaration:
|
||||
// case ts.SyntaxKind.ExportDeclaration:
|
||||
// case ts.SyntaxKind.Constructor:
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, node => visit(checker, sourceFile, file, node))
|
||||
}
|
||||
|
||||
function generateMetadata(
|
||||
out: string,
|
||||
themedir: string,
|
||||
fileNames: string[],
|
||||
): void {
|
||||
/* Parse Tridactyl */
|
||||
let program = ts.createProgram(fileNames, {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
})
|
||||
|
||||
let metadata = new AllMetadata.ProgramMetadata()
|
||||
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
let name = (fileNames as any).find(name =>
|
||||
sourceFile.fileName.match(name),
|
||||
)
|
||||
if (name) {
|
||||
let file = metadata.getFile(name)
|
||||
if (!file) {
|
||||
file = new AllMetadata.FileMetadata()
|
||||
metadata.setFile(name, file)
|
||||
}
|
||||
visit(program.getTypeChecker(), sourceFile, file, sourceFile)
|
||||
}
|
||||
}
|
||||
|
||||
// We need to specify Type itself because it won't exist in AllTypes.js since it's an interface
|
||||
let imports =
|
||||
`import { Type } from "../compiler/types/AllTypes"\n` +
|
||||
`import {${Object.keys(AllTypes).join(
|
||||
", ",
|
||||
)}} from "../compiler/types/AllTypes"\n` +
|
||||
`import {${Object.keys(AllMetadata).join(
|
||||
", ",
|
||||
)}} from "../compiler/metadata/AllMetadata"\n`
|
||||
|
||||
let metadataString =
|
||||
imports + `\nexport let everything = ${metadata.toConstructor()}\n`
|
||||
|
||||
if (themedir) {
|
||||
metadataString += `\nexport let staticThemes = ${JSON.stringify(
|
||||
fs.readdirSync(themedir),
|
||||
)}\n`
|
||||
}
|
||||
|
||||
// print out the doc
|
||||
fs.writeFileSync(out, metadataString)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let opts = commandLineArgs([
|
||||
{ name: "out", type: String },
|
||||
{ name: "themeDir", type: String },
|
||||
{ name: "src", type: String, multiple: true, defaultOption: true },
|
||||
])
|
||||
|
||||
if (!opts.out || opts.src.length < 1)
|
||||
throw new Error(
|
||||
"Argument syntax: --out outfile [--src] file1.ts [file2.ts ...]",
|
||||
)
|
||||
|
||||
generateMetadata(opts.out, opts.themeDir, opts.src)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export { SymbolMetadata } from "./SymbolMetadata"
|
||||
export { ClassMetadata } from "./ClassMetadata"
|
||||
export { FileMetadata } from "./FileMetadata"
|
||||
export { ProgramMetadata } from "./ProgramMetadata"
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
|
||||
import { SymbolMetadata } from "./SymbolMetadata"
|
||||
|
||||
export class ClassMetadata {
|
||||
constructor(
|
||||
public members: Map<string, SymbolMetadata> = new Map<
|
||||
string,
|
||||
SymbolMetadata
|
||||
>(),
|
||||
) {}
|
||||
|
||||
public setMember(name: string, s: SymbolMetadata) {
|
||||
this.members.set(name, s)
|
||||
}
|
||||
|
||||
public getMember(name: string) {
|
||||
return this.members.get(name)
|
||||
}
|
||||
|
||||
public getMembers() {
|
||||
return this.members.keys()
|
||||
}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new ClassMetadata(new Map<string, SymbolMetadata>([` +
|
||||
Array.from(this.members.entries())
|
||||
.map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
|
||||
.join(",\n") +
|
||||
`]))`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { ClassMetadata } from "./ClassMetadata"
|
||||
import { SymbolMetadata } from "./SymbolMetadata"
|
||||
|
||||
export class FileMetadata {
|
||||
constructor(
|
||||
public classes: Map<string, ClassMetadata> = new Map<
|
||||
string,
|
||||
ClassMetadata
|
||||
>(),
|
||||
public functions: Map<string, SymbolMetadata> = new Map<
|
||||
string,
|
||||
SymbolMetadata
|
||||
>(),
|
||||
) {}
|
||||
|
||||
public setClass(name: string, c: ClassMetadata) {
|
||||
this.classes.set(name, c)
|
||||
}
|
||||
|
||||
public getClass(name: string) {
|
||||
return this.classes.get(name)
|
||||
}
|
||||
|
||||
public getClasses() {
|
||||
return Array.from(this.classes.keys())
|
||||
}
|
||||
|
||||
public setFunction(name: string, f: SymbolMetadata) {
|
||||
this.functions.set(name, f)
|
||||
}
|
||||
|
||||
public getFunction(name: string) {
|
||||
return this.functions.get(name)
|
||||
}
|
||||
|
||||
public getFunctions() {
|
||||
return Array.from(this.functions.entries())
|
||||
}
|
||||
|
||||
public getFunctionNames() {
|
||||
return Array.from(this.functions.keys())
|
||||
}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new FileMetadata(new Map<string, ClassMetadata>([` +
|
||||
Array.from(this.classes.entries())
|
||||
.map(([n, c]) => `[${JSON.stringify(n)}, ${c.toConstructor()}]`)
|
||||
.join(",\n") +
|
||||
`]), new Map<string, SymbolMetadata>([` +
|
||||
Array.from(this.functions.entries())
|
||||
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
|
||||
.join(",\n") +
|
||||
`]))`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { FileMetadata } from "./FileMetadata"
|
||||
|
||||
export class ProgramMetadata {
|
||||
constructor(
|
||||
public files: Map<string, FileMetadata> = new Map<
|
||||
string,
|
||||
FileMetadata
|
||||
>(),
|
||||
) {}
|
||||
|
||||
public setFile(name: string, file: FileMetadata) {
|
||||
this.files.set(name, file)
|
||||
}
|
||||
|
||||
public getFile(name: string) {
|
||||
return this.files.get(name)
|
||||
}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new ProgramMetadata(new Map<string, FileMetadata>([` +
|
||||
Array.from(this.files.entries())
|
||||
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
|
||||
.join(",\n") +
|
||||
`]))`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { Type } from "../types/AllTypes"
|
||||
|
||||
export class SymbolMetadata {
|
||||
constructor(public doc: string, public type: Type, public hidden = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new SymbolMetadata(${JSON.stringify(
|
||||
this.doc,
|
||||
)}, ${this.type.toConstructor()}, ${this.hidden})`
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
export { Type } from "./Type"
|
||||
export { AnyType } from "./AnyType"
|
||||
export { BooleanType } from "./BooleanType"
|
||||
export { FunctionType } from "./FunctionType"
|
||||
export { NumberType } from "./NumberType"
|
||||
export { ObjectType } from "./ObjectType"
|
||||
export { StringType } from "./StringType"
|
||||
export { TypeReferenceType } from "./TypeReferenceType"
|
||||
export { VoidType } from "./VoidType"
|
||||
export { ArrayType } from "./ArrayType"
|
||||
export { LiteralTypeType } from "./LiteralTypeType"
|
||||
export { TupleType } from "./TupleType"
|
||||
export { UnionType } from "./UnionType"
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class AnyType implements Type {
|
||||
public kind = "any"
|
||||
|
||||
constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new AnyType(${!this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
return argument
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class ArrayType implements Type {
|
||||
public kind = "array"
|
||||
|
||||
constructor(public elemType: Type, public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new ArrayType(${this.elemType.toConstructor()}, ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `${this.elemType.toString()}[]`
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (!Array.isArray(argument)) {
|
||||
try {
|
||||
argument = JSON.parse(argument)
|
||||
} catch (e) {
|
||||
throw new Error(`Can't convert ${argument} to array:`)
|
||||
}
|
||||
if (!Array.isArray(argument)) {
|
||||
throw new Error(`Can't convert ${argument} to array:`)
|
||||
}
|
||||
}
|
||||
return argument.map(v => this.elemType.convert(v))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class BooleanType implements Type {
|
||||
public kind = "boolean"
|
||||
|
||||
constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new BooleanType(${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (argument === "true") {
|
||||
return true
|
||||
} else if (argument === "false") {
|
||||
return false
|
||||
}
|
||||
throw new Error("Can't convert ${argument} to boolean")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class FunctionType implements Type {
|
||||
public kind = "function"
|
||||
|
||||
constructor(public args: Type[], public ret: Type, public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new FunctionType([` +
|
||||
// Convert every argument type to its string constructor representation
|
||||
this.args.map(cur => cur.toConstructor()) +
|
||||
`], ${this.ret.toConstructor()}, ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `(${this.args.map(a => a.toString()).join(", ")}) => ${this.ret.toString()}`
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
// Possible strategies:
|
||||
// - eval()
|
||||
// - window[argument]
|
||||
// - tri.excmds[argument]
|
||||
throw new Error(`Conversion to function not implemented: ${argument}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class LiteralTypeType implements Type {
|
||||
public kind = "LiteralType"
|
||||
|
||||
constructor(public value: string, public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new LiteralTypeType(${JSON.stringify(this.value)}, ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return JSON.stringify(this.value)
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (argument === this.value) {
|
||||
return argument
|
||||
}
|
||||
throw new Error(
|
||||
`Argument does not match expected value (${
|
||||
this.value
|
||||
}): ${argument}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class NumberType implements Type {
|
||||
public kind = "number"
|
||||
|
||||
public constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new NumberType(${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
const n = parseFloat(argument)
|
||||
if (!Number.isNaN(n)) {
|
||||
return n
|
||||
}
|
||||
throw new Error(`Can't convert to number: ${argument}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class ObjectType implements Type {
|
||||
public kind = "object"
|
||||
|
||||
// Note: a map that has an empty key ("") uses the corresponding type as default type
|
||||
constructor(public members: Map<string, Type> = new Map<string, Type>(), public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new ObjectType(new Map<string, Type>([` +
|
||||
Array.from(this.members.entries()).map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
|
||||
.join(", ") +
|
||||
`]), ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convertMember(memberName: string[], memberValue: string) {
|
||||
let type = this.members.get(memberName[0])
|
||||
if (!type) {
|
||||
// No type, try to get the default type
|
||||
type = this.members.get("")
|
||||
if (!type) {
|
||||
// No info for this member and no default type, anything goes
|
||||
return memberValue
|
||||
}
|
||||
}
|
||||
if (type.kind === "object") {
|
||||
return (type as ObjectType).convertMember(memberName.slice(1), memberValue)
|
||||
}
|
||||
return type.convert(memberValue)
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
try {
|
||||
return JSON.parse(argument)
|
||||
} catch (e) {
|
||||
throw new Error(`Can't convert to object: ${argument}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class StringType implements Type {
|
||||
public kind = "string"
|
||||
|
||||
constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new StringType(${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (typeof argument === "string") {
|
||||
return argument
|
||||
}
|
||||
throw new Error(`Can't convert to string: ${argument}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class TupleType implements Type {
|
||||
public kind = "tuple"
|
||||
|
||||
constructor(public elemTypes: Type[], public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new TupleType([` +
|
||||
// Convert every element type to its constructor representation
|
||||
this.elemTypes.map(cur => cur.toConstructor()).join(",\n") +
|
||||
`], ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `[${this.elemTypes.map(e => e.toString()).join(", ")}]`
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (!Array.isArray(argument)) {
|
||||
try {
|
||||
argument = JSON.parse(argument)
|
||||
} catch (e) {
|
||||
throw new Error(`Can't convert to tuple: ${argument}`)
|
||||
}
|
||||
if (!Array.isArray(argument)) {
|
||||
throw new Error(`Can't convert to tuple: ${argument}`)
|
||||
}
|
||||
}
|
||||
if (argument.length !== this.elemTypes.length) {
|
||||
throw new Error(
|
||||
`Error converting tuple: number of elements and type mismatch ${argument}`,
|
||||
)
|
||||
}
|
||||
return argument.map((v, i) => this.elemTypes[i].convert(v))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
|
||||
export interface Type {
|
||||
// Only available on argument types
|
||||
name?: string
|
||||
isDotDotDot?: boolean
|
||||
isQuestion?: boolean
|
||||
// available everywhere
|
||||
kind: string
|
||||
toConstructor(): string
|
||||
toString(): string
|
||||
convert(argument: string): any
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class TypeReferenceType implements Type {
|
||||
public constructor(public kind: string, public args: Type[], public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new TypeReferenceType(${JSON.stringify(this.kind)}, [` +
|
||||
// Turn every type argument into its constructor representation
|
||||
this.args.map(cur => cur.toConstructor()).join(",\n") +
|
||||
`], ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `${this.kind}<${this.args.map(a => a.toString()).join(", ")}>`
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
throw new Error("Conversion of simple type references not implemented.")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class UnionType implements Type {
|
||||
public kind = "union"
|
||||
|
||||
constructor(public types: Type[], public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new UnionType([` +
|
||||
// Convert every type to its string constructor representation
|
||||
this.types.map(cur => cur.toConstructor()).join(",\n") +
|
||||
`], ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.types.map(t => t.toString()).join(" | ")
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
for (const t of this.types) {
|
||||
try {
|
||||
return t.convert(argument)
|
||||
} catch (e) {}
|
||||
}
|
||||
throw new Error(`Can't convert "${argument}" to any of: ${this.types}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class VoidType implements Type {
|
||||
public kind = "void"
|
||||
|
||||
constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new VoidType(${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -57,13 +57,15 @@ if [ "$QUICK_BUILD" != "1" ]; then
|
|||
"$(yarn bin)/nearleyc" src/grammars/bracketexpr.ne > \
|
||||
src/grammars/.bracketexpr.generated.ts
|
||||
|
||||
# It's important to generate the metadata before the documentation because
|
||||
# missing imports might break documentation generation on clean builds
|
||||
"$(yarn bin)/tsc" compiler/gen_metadata.ts -m commonjs --target es2017 \
|
||||
&& node compiler/gen_metadata.js \
|
||||
--out src/.metadata.generated.ts \
|
||||
--themeDir src/static/themes \
|
||||
src/excmds.ts src/lib/config.ts
|
||||
# Generate runtime metadata via typedoc. src/lib/metadata.ts loads the
|
||||
# JSON and exposes the ProgramMetadata/FileMetadata/Type surface used at
|
||||
# runtime; the .metadata.generated.ts shim below routes the public
|
||||
# @src/.metadata.generated import path to that loader.
|
||||
"$(yarn bin)/typedoc" --json src/.metadata.generated.json --mode file \
|
||||
--exclude 'src/.excmds_*.generated.ts' --ignoreCompilerErrors \
|
||||
src/excmds.ts src/lib/config.ts
|
||||
node -e "var fs=require('fs');fs.writeFileSync('src/.themes.generated.json',JSON.stringify(fs.readdirSync('src/static/themes')))"
|
||||
printf 'export * from "./lib/metadata"\n' > src/.metadata.generated.ts
|
||||
|
||||
scripts/newtab.md.sh
|
||||
scripts/make_tutorial.sh
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as Metadata from "@src/.metadata.generated"
|
||||
import {
|
||||
excmdsFunctions,
|
||||
defaultConfigMembers,
|
||||
getDoc,
|
||||
memberDoc,
|
||||
} from "@src/.metadata.generated"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
import * as config from "@src/lib/config"
|
||||
|
||||
class AproposCompletionOption extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
class AproposCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse
|
||||
{
|
||||
public fuseKeys = []
|
||||
|
||||
constructor(public name: string, doc: string, flag: string) {
|
||||
constructor(
|
||||
public name: string,
|
||||
doc: string,
|
||||
flag: string,
|
||||
) {
|
||||
super()
|
||||
this.value = `${flag} ${name}`
|
||||
this.html = html`<tr class="AproposCompletionOption option">
|
||||
|
|
@ -42,20 +53,13 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
|||
return
|
||||
}
|
||||
|
||||
const file = Metadata.everything.getFile("src/lib/config.ts")
|
||||
const default_config = file.getClass("default_config")
|
||||
const excmds = Metadata.everything.getFile("src/excmds.ts")
|
||||
const fns = excmds.getFunctions()
|
||||
const settings = config.get()
|
||||
const exaliases = settings.exaliases
|
||||
const bindings = settings.nmaps
|
||||
if (
|
||||
fns === undefined ||
|
||||
exaliases === undefined ||
|
||||
bindings === undefined
|
||||
) {
|
||||
if (exaliases === undefined || bindings === undefined) {
|
||||
return
|
||||
}
|
||||
const fns = Object.entries(excmdsFunctions)
|
||||
|
||||
const flags = {
|
||||
"-a": (options, query) =>
|
||||
|
|
@ -65,16 +69,14 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
|||
(
|
||||
alias +
|
||||
aliases.expandExstr(alias) +
|
||||
excmds.getFunction(aliases.expandExstr(alias))
|
||||
excmdsFunctions[aliases.expandExstr(alias)]
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(query),
|
||||
)
|
||||
.map(alias => {
|
||||
const cmd = aliases.expandExstr(alias)
|
||||
const doc =
|
||||
(excmds.getFunction(cmd) || ({} as any)).doc ||
|
||||
""
|
||||
const doc = getDoc(excmdsFunctions[cmd])
|
||||
return new AproposCompletionOption(
|
||||
alias,
|
||||
`Alias for \`${cmd}\`. ${doc}`,
|
||||
|
|
@ -102,16 +104,14 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
|||
"-e": (options, query) =>
|
||||
options.concat(
|
||||
fns
|
||||
.filter(
|
||||
([name, fn]) =>
|
||||
!fn.hidden &&
|
||||
(name + fn.doc).toLowerCase().includes(query),
|
||||
.filter(([name, fn]) =>
|
||||
(name + getDoc(fn)).toLowerCase().includes(query),
|
||||
)
|
||||
.map(
|
||||
([name, fn]) =>
|
||||
new AproposCompletionOption(
|
||||
name,
|
||||
`Excmd. ${fn.doc}`,
|
||||
`Excmd. ${getDoc(fn)}`,
|
||||
"-e",
|
||||
),
|
||||
),
|
||||
|
|
@ -120,16 +120,12 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
|||
options.concat(
|
||||
Object.keys(settings)
|
||||
.filter(x =>
|
||||
(x + default_config.getMember(x)?.doc)
|
||||
(x + memberDoc(defaultConfigMembers[x]))
|
||||
.toLowerCase()
|
||||
.includes(query),
|
||||
)
|
||||
.map(setting => {
|
||||
const member = default_config.getMember(setting)
|
||||
let doc = ""
|
||||
if (member !== undefined) {
|
||||
doc = member.doc
|
||||
}
|
||||
const doc = memberDoc(defaultConfigMembers[setting])
|
||||
return new AproposCompletionOption(
|
||||
setting,
|
||||
`Setting. ${doc}`,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as ExcmdCompletions from "@src/completions/Excmd"
|
||||
import * as Metadata from "@src/.metadata.generated"
|
||||
import { excmdsFunctions, getDoc } from "@src/.metadata.generated"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
|
||||
|
|
@ -65,21 +65,15 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
|
|||
return
|
||||
}
|
||||
|
||||
const excmds = Metadata.everything.getFile("src/excmds.ts")
|
||||
if (!excmds) return
|
||||
const fns = excmds.getFunctions()
|
||||
|
||||
// Add all excmds that start with exstr and that tridactyl has metadata about to completions
|
||||
this.options = this.scoreOptions(
|
||||
fns
|
||||
.filter(
|
||||
([name, fn]) => !fn.hidden && name.startsWith(end_exstr),
|
||||
)
|
||||
Object.entries(excmdsFunctions)
|
||||
.filter(([name]) => name.startsWith(end_exstr))
|
||||
.map(
|
||||
([name, fn]) =>
|
||||
new ExcmdCompletions.ExcmdCompletionOption(
|
||||
name,
|
||||
fn.doc,
|
||||
getDoc(fn),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -90,12 +84,12 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
|
|||
)
|
||||
for (const alias of exaliases) {
|
||||
const cmd = aliases.expandExstr(alias)
|
||||
const fn = excmds.getFunction(cmd)
|
||||
const fn = excmdsFunctions[cmd]
|
||||
if (fn) {
|
||||
this.options.push(
|
||||
new ExcmdCompletions.ExcmdCompletionOption(
|
||||
alias,
|
||||
`Alias for \`${cmd}\`. ${fn.doc}`,
|
||||
`Alias for \`${cmd}\`. ${getDoc(fn)}`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as Metadata from "@src/.metadata.generated"
|
||||
import { excmdsFunctions, getDoc } from "@src/.metadata.generated"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
|
||||
export class ExcmdCompletionOption extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
export class ExcmdCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse
|
||||
{
|
||||
public fuseKeys = []
|
||||
constructor(public value: string, public documentation: string = "") {
|
||||
constructor(
|
||||
public value: string,
|
||||
public documentation: string = "",
|
||||
) {
|
||||
super()
|
||||
this.fuseKeys.push(this.value)
|
||||
|
||||
|
|
@ -58,15 +63,13 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
private async updateOptions(exstr = "") {
|
||||
this.lastExstr = exstr
|
||||
|
||||
const excmds = Metadata.everything.getFile("src/excmds.ts")
|
||||
if (!excmds) return
|
||||
const fns = excmds.getFunctions()
|
||||
|
||||
// Add all excmds that start with exstr and that tridactyl has metadata about to completions
|
||||
this.options = this.scoreOptions(
|
||||
fns
|
||||
.filter(([name, fn]) => !fn.hidden && name.startsWith(exstr))
|
||||
.map(([name, fn]) => new ExcmdCompletionOption(name, fn.doc)),
|
||||
Object.entries(excmdsFunctions)
|
||||
.filter(([name]) => name.startsWith(exstr))
|
||||
.map(
|
||||
([name, fn]) => new ExcmdCompletionOption(name, getDoc(fn)),
|
||||
),
|
||||
)
|
||||
|
||||
// Also narrow down aliases map to possible completions
|
||||
|
|
@ -80,13 +83,13 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
|
||||
for (const alias of Object.keys(exaliases)) {
|
||||
const cmd = aliases.expandExstr(alias, exaliases)
|
||||
const fn = excmds.getFunction(cmd)
|
||||
const fn = excmdsFunctions[cmd]
|
||||
|
||||
if (fn) {
|
||||
this.options.push(
|
||||
new ExcmdCompletionOption(
|
||||
alias,
|
||||
`Alias for \`${cmd}\`. ${fn.doc}`,
|
||||
`Alias for \`${cmd}\`. ${getDoc(fn)}`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
|
@ -100,12 +103,11 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
// Add partial matched funcs like: 'conf' ~= 'viewconfig'
|
||||
const seen = new Set(this.options.map(o => o.value))
|
||||
const partial_options = this.scoreOptions(
|
||||
fns
|
||||
.filter(
|
||||
([name, fn]) =>
|
||||
!fn.hidden && name.includes(exstr) && !seen.has(name),
|
||||
)
|
||||
.map(([name, fn]) => new ExcmdCompletionOption(name, fn.doc)),
|
||||
Object.entries(excmdsFunctions)
|
||||
.filter(([name]) => name.includes(exstr) && !seen.has(name))
|
||||
.map(
|
||||
([name, fn]) => new ExcmdCompletionOption(name, getDoc(fn)),
|
||||
),
|
||||
)
|
||||
this.options = this.options.concat(partial_options)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as Metadata from "@src/.metadata.generated"
|
||||
import {
|
||||
excmdsFunctions,
|
||||
defaultConfigMembers,
|
||||
getDoc,
|
||||
memberDoc,
|
||||
} from "@src/.metadata.generated"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
import * as config from "@src/lib/config"
|
||||
|
||||
class HelpCompletionOption extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
class HelpCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse
|
||||
{
|
||||
public fuseKeys = []
|
||||
|
||||
constructor(public name: string, doc: string, flag: string) {
|
||||
constructor(
|
||||
public name: string,
|
||||
doc: string,
|
||||
flag: string,
|
||||
) {
|
||||
super()
|
||||
this.value = `${flag} ${name}`
|
||||
this.html = html`<tr class="HelpCompletionOption option">
|
||||
|
|
@ -42,20 +53,13 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
return
|
||||
}
|
||||
|
||||
const file = Metadata.everything.getFile("src/lib/config.ts")
|
||||
const default_config = file.getClass("default_config")
|
||||
const excmds = Metadata.everything.getFile("src/excmds.ts")
|
||||
const fns = excmds.getFunctions()
|
||||
const settings = config.get()
|
||||
const exaliases = settings.exaliases
|
||||
const bindings = settings.nmaps
|
||||
if (
|
||||
fns === undefined ||
|
||||
exaliases === undefined ||
|
||||
bindings === undefined
|
||||
) {
|
||||
if (exaliases === undefined || bindings === undefined) {
|
||||
return
|
||||
}
|
||||
const fns = Object.entries(excmdsFunctions)
|
||||
|
||||
const flags = {
|
||||
"-a": (options, query) =>
|
||||
|
|
@ -64,9 +68,7 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
.filter(alias => alias.startsWith(query))
|
||||
.map(alias => {
|
||||
const cmd = aliases.expandExstr(alias)
|
||||
const doc =
|
||||
(excmds.getFunction(cmd) || ({} as any)).doc ||
|
||||
""
|
||||
const doc = getDoc(excmdsFunctions[cmd])
|
||||
return new HelpCompletionOption(
|
||||
alias,
|
||||
`Alias for \`${cmd}\`. ${doc}`,
|
||||
|
|
@ -90,15 +92,12 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
"-e": (options, query) =>
|
||||
options.concat(
|
||||
fns
|
||||
.filter(
|
||||
([name, fn]) =>
|
||||
!fn.hidden && name.startsWith(query),
|
||||
)
|
||||
.filter(([name]) => name.startsWith(query))
|
||||
.map(
|
||||
([name, fn]) =>
|
||||
new HelpCompletionOption(
|
||||
name,
|
||||
`Excmd. ${fn.doc}`,
|
||||
`Excmd. ${getDoc(fn)}`,
|
||||
"-e",
|
||||
),
|
||||
),
|
||||
|
|
@ -108,11 +107,7 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
Object.keys(settings)
|
||||
.filter(x => x.startsWith(query))
|
||||
.map(setting => {
|
||||
const member = default_config.getMember(setting)
|
||||
let doc = ""
|
||||
if (member !== undefined) {
|
||||
doc = member.doc
|
||||
}
|
||||
const doc = memberDoc(defaultConfigMembers[setting])
|
||||
return new HelpCompletionOption(
|
||||
setting,
|
||||
`Setting. ${doc}`,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as metadata from "@src/.metadata.generated"
|
||||
import {
|
||||
defaultConfigMembers,
|
||||
memberDoc,
|
||||
memberType,
|
||||
typeToString,
|
||||
} from "@src/.metadata.generated"
|
||||
|
||||
class SettingsCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
implements Completions.CompletionOptionFuse
|
||||
{
|
||||
public fuseKeys = []
|
||||
|
||||
constructor(
|
||||
|
|
@ -66,11 +72,9 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse {
|
|||
|
||||
options += options ? " " : ""
|
||||
|
||||
const file = metadata.everything.getFile("src/lib/config.ts")
|
||||
const default_config = file.getClass("default_config")
|
||||
const settings = config.get()
|
||||
|
||||
if (default_config === undefined || settings === undefined) {
|
||||
if (settings === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -78,18 +82,12 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse {
|
|||
.filter(x => x.startsWith(query))
|
||||
.sort()
|
||||
.map(setting => {
|
||||
const md = default_config.getMember(setting)
|
||||
let doc = ""
|
||||
let type = ""
|
||||
if (md !== undefined) {
|
||||
doc = md.doc
|
||||
type = md.type.toString()
|
||||
}
|
||||
const md = defaultConfigMembers[setting]
|
||||
return new SettingsCompletionOption(options + setting, {
|
||||
name: setting,
|
||||
value: JSON.stringify(settings[setting]),
|
||||
doc,
|
||||
type,
|
||||
doc: memberDoc(md),
|
||||
type: md ? typeToString(memberType(md)) : "",
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ import * as Logging from "@src/lib/logging"
|
|||
import { AutoContain } from "@src/lib/autocontainers"
|
||||
import * as CSS from "css"
|
||||
import * as Perf from "@src/perf"
|
||||
import * as Metadata from "@src/.metadata.generated"
|
||||
import { ObjectType } from "../compiler/types/ObjectType"
|
||||
import { staticThemes, defaultConfigMembers, memberType, typeKind, convert, convertMember } from "@src/.metadata.generated"
|
||||
import * as Native from "@src/lib/native"
|
||||
import * as TTS from "@src/lib/text_to_speech"
|
||||
import * as excmd_parser from "@src/parsers/exmode"
|
||||
|
|
@ -544,7 +543,7 @@ export async function colourscheme(...args: string[]) {
|
|||
const themename = option._[0]
|
||||
|
||||
// If this is a builtin theme, no need to bother with slow stuff
|
||||
if (!Metadata.staticThemes.includes(themename)) {
|
||||
if (!staticThemes.includes(themename)) {
|
||||
if (themename.search("\\.") >= 0) throw new Error(`Theme name should not contain any dots! (given name: ${themename}).`)
|
||||
if (url) {
|
||||
if (themename === undefined) throw new Error(`You must provide a theme name!`)
|
||||
|
|
@ -1427,6 +1426,7 @@ export function scrollto(a: number | string, b: number | "x" | "y" = "y") {
|
|||
/** @hidden */
|
||||
//#content_helper
|
||||
let lineHeight = null
|
||||
|
||||
/** Scrolls the document of its first scrollable child element by n lines.
|
||||
*
|
||||
* The height of a line is defined by the site's CSS. If Tridactyl can't get it, it'll default to 22 pixels.
|
||||
|
|
@ -4591,16 +4591,15 @@ function validateSetArgs(key: string, values: string[]) {
|
|||
const target: any[] = key.split(".")
|
||||
|
||||
let value
|
||||
const file = Metadata.everything.getFile("src/lib/config.ts")
|
||||
const default_config = file.getClass("default_config")
|
||||
const md = default_config.getMember(target[0])
|
||||
const md = defaultConfigMembers[target[0]]
|
||||
if (md !== undefined) {
|
||||
const strval = values.join(" ")
|
||||
const t = memberType(md)
|
||||
// Note: the conversion will throw if strval can't be converted to the right type
|
||||
if (md.type.kind === "object" && target.length > 1) {
|
||||
value = (md.type as ObjectType).convertMember(target.slice(1), strval)
|
||||
if (typeKind(t) === "object" && target.length > 1) {
|
||||
value = convertMember(t, target.slice(1), strval)
|
||||
} else {
|
||||
value = md.type.convert(strval)
|
||||
value = convert(t, strval)
|
||||
}
|
||||
} else {
|
||||
// If we don't have metadata, fall back to the old way
|
||||
|
|
|
|||
320
src/lib/metadata.ts
Normal file
320
src/lib/metadata.ts
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
/**
|
||||
* Runtime metadata over `typedoc --json` output for src/excmds.ts and
|
||||
* src/lib/config.ts. Exposes plain-object indexes (keyed by symbol name) plus
|
||||
* a small set of free helpers that operate on raw typedoc nodes.
|
||||
*
|
||||
* @hidden symbols are dropped by typedoc itself before they reach this loader,
|
||||
* so consumers never see them.
|
||||
*/
|
||||
|
||||
import metadataJson from "../.metadata.generated.json"
|
||||
import staticThemesJson from "../.themes.generated.json"
|
||||
|
||||
export const staticThemes: string[] = staticThemesJson
|
||||
|
||||
// =============================================================================
|
||||
// Indexes — built once at module load by walking the typedoc tree.
|
||||
// =============================================================================
|
||||
|
||||
type Node = any
|
||||
|
||||
const fileBuckets: Record<
|
||||
string,
|
||||
{ functions: Record<string, Node>; classes: Record<string, Node> }
|
||||
> = {
|
||||
"src/excmds.ts": { functions: {}, classes: {} },
|
||||
"src/lib/config.ts": { functions: {}, classes: {} },
|
||||
}
|
||||
|
||||
function inTargetFile(node: Node): string | undefined {
|
||||
for (const src of node.sources || []) {
|
||||
if (fileBuckets[src.fileName]) return src.fileName
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function walk(node: Node) {
|
||||
if (!node || typeof node !== "object") return
|
||||
if (Array.isArray(node)) {
|
||||
for (const v of node) walk(v)
|
||||
return
|
||||
}
|
||||
if (node.kindString === "Function") {
|
||||
const f = inTargetFile(node)
|
||||
if (f) fileBuckets[f].functions[node.name] = node
|
||||
} else if (node.kindString === "Class") {
|
||||
const f = inTargetFile(node)
|
||||
if (f) fileBuckets[f].classes[node.name] = node
|
||||
}
|
||||
if (node.children) walk(node.children)
|
||||
}
|
||||
walk(metadataJson)
|
||||
|
||||
export const excmdsFunctions: Record<string, Node> =
|
||||
fileBuckets["src/excmds.ts"].functions
|
||||
|
||||
const defaultConfig: Node | undefined =
|
||||
fileBuckets["src/lib/config.ts"].classes["default_config"]
|
||||
|
||||
export const defaultConfigMembers: Record<string, Node> = (() => {
|
||||
const out: Record<string, Node> = {}
|
||||
for (const ch of defaultConfig?.children || []) out[ch.name] = ch
|
||||
return out
|
||||
})()
|
||||
|
||||
// =============================================================================
|
||||
// Doc / type access on function and class-member nodes.
|
||||
// =============================================================================
|
||||
|
||||
function readComment(c: Node | undefined): string {
|
||||
if (!c) return ""
|
||||
let s: string = c.shortText || ""
|
||||
if (c.text) s += (s ? "\n\n" : "") + c.text
|
||||
return s.replace(/\n+$/, "")
|
||||
}
|
||||
|
||||
export function getDoc(node: Node | undefined): string {
|
||||
if (!node) return ""
|
||||
return (
|
||||
readComment(node.signatures?.[0]?.comment) || readComment(node.comment)
|
||||
)
|
||||
}
|
||||
|
||||
export function memberDoc(node: Node | undefined): string {
|
||||
if (!node) return ""
|
||||
if (node.kindString === "Accessor") {
|
||||
const sig = (node.getSignature || [])[0] || (node.setSignature || [])[0]
|
||||
if (sig?.comment) return readComment(sig.comment)
|
||||
}
|
||||
return readComment(node.comment)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a typedoc type node for a class member, synthesising an empty
|
||||
* reflection for `Object literal` fields (which have no annotated type) so
|
||||
* downstream helpers can treat them uniformly as objects.
|
||||
*/
|
||||
export function memberType(node: Node | undefined): Node | undefined {
|
||||
if (!node) return undefined
|
||||
if (node.type) return node.type
|
||||
if (node.kindString === "Object literal") {
|
||||
return { type: "reflection", declaration: { children: [] } }
|
||||
}
|
||||
if (node.kindString === "Accessor") {
|
||||
const sig = (node.getSignature || [])[0] || (node.setSignature || [])[0]
|
||||
return sig?.type
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Parameter list for a function node — each entry has `name`, `type`, `flags`. */
|
||||
export function paramTypes(fnNode: Node | undefined): Node[] {
|
||||
return fnNode?.signatures?.[0]?.parameters || []
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Type coercion + stringification over typedoc type nodes.
|
||||
// =============================================================================
|
||||
|
||||
function intrinsicName(t) {
|
||||
switch (t.name) {
|
||||
case "string":
|
||||
case "number":
|
||||
case "boolean":
|
||||
case "object":
|
||||
return t.name
|
||||
case "void":
|
||||
case "undefined":
|
||||
case "never":
|
||||
return "void"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalised kind: "string" | "number" | "boolean" | "object" | "array" | "void" | "any" | ... */
|
||||
export function typeKind(t: Node | undefined): string {
|
||||
if (!t) return "any"
|
||||
switch (t.type) {
|
||||
case "intrinsic":
|
||||
return intrinsicName(t)
|
||||
case "array":
|
||||
case "tuple":
|
||||
case "union":
|
||||
return t.type
|
||||
case "reflection":
|
||||
return t.declaration?.signatures?.length ? "function" : "object"
|
||||
case "stringLiteral":
|
||||
case "numberLiteral":
|
||||
case "booleanLiteral":
|
||||
return "LiteralType"
|
||||
case "reference":
|
||||
return t.name || "reference"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
|
||||
export function typeToString(t: Node | undefined): string {
|
||||
if (!t) return "any"
|
||||
switch (t.type) {
|
||||
case "intrinsic":
|
||||
return t.name
|
||||
case "array":
|
||||
return `${typeToString(t.elementType)}[]`
|
||||
case "tuple":
|
||||
return `[${(t.elements || []).map(typeToString).join(", ")}]`
|
||||
case "union":
|
||||
return (t.types || []).map(typeToString).join(" | ")
|
||||
case "stringLiteral":
|
||||
case "numberLiteral":
|
||||
case "booleanLiteral":
|
||||
return JSON.stringify(t.value)
|
||||
case "reference": {
|
||||
const args = (t.typeArguments || []).map(typeToString)
|
||||
return args.length
|
||||
? `${t.name}<${args.join(", ")}>`
|
||||
: t.name || "reference"
|
||||
}
|
||||
case "reflection": {
|
||||
const decl = t.declaration || {}
|
||||
if (decl.signatures?.length) {
|
||||
const sig = decl.signatures[0]
|
||||
const args = (sig.parameters || []).map((p: Node) =>
|
||||
typeToString(p.type),
|
||||
)
|
||||
return `(${args.join(", ")}) => ${typeToString(sig.type)}`
|
||||
}
|
||||
return "object"
|
||||
}
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
|
||||
function convertIntrinsic(t) {
|
||||
switch (t.name) {
|
||||
case "string":
|
||||
if (typeof value === "string") return value
|
||||
throw new Error(`Can't convert to string: ${value}`)
|
||||
case "number": {
|
||||
const n = parseFloat(value)
|
||||
if (!Number.isNaN(n)) return n
|
||||
throw new Error(`Can't convert to number: ${value}`)
|
||||
}
|
||||
case "boolean":
|
||||
if (value === "true") return true
|
||||
if (value === "false") return false
|
||||
throw new Error(`Can't convert ${value} to boolean`)
|
||||
case "object":
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
throw new Error(`Can't convert to object: ${value}`)
|
||||
}
|
||||
case "void":
|
||||
case "undefined":
|
||||
case "never":
|
||||
return null
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function convert(t: Node | undefined, value: any): any {
|
||||
if (!t) return value
|
||||
switch (t.type) {
|
||||
case "intrinsic":
|
||||
return convertIntrinsic(t)
|
||||
case "array": {
|
||||
let arr = value
|
||||
if (!Array.isArray(arr)) {
|
||||
try {
|
||||
arr = JSON.parse(arr)
|
||||
} catch {
|
||||
throw new Error(`Can't convert ${value} to array:`)
|
||||
}
|
||||
if (!Array.isArray(arr))
|
||||
throw new Error(`Can't convert ${value} to array:`)
|
||||
}
|
||||
return arr.map((v: any) => convert(t.elementType, v))
|
||||
}
|
||||
case "tuple": {
|
||||
let arr = value
|
||||
if (!Array.isArray(arr)) {
|
||||
try {
|
||||
arr = JSON.parse(arr)
|
||||
} catch {
|
||||
throw new Error(`Can't convert to tuple: ${value}`)
|
||||
}
|
||||
if (!Array.isArray(arr))
|
||||
throw new Error(`Can't convert to tuple: ${value}`)
|
||||
}
|
||||
const elems = t.elements || []
|
||||
if (arr.length !== elems.length) {
|
||||
throw new Error(
|
||||
`Error converting tuple: number of elements and type mismatch ${value}`,
|
||||
)
|
||||
}
|
||||
return arr.map((v: any, i: number) => convert(elems[i], v))
|
||||
}
|
||||
case "union":
|
||||
for (const u of t.types || []) {
|
||||
try {
|
||||
return convert(u, value)
|
||||
} catch {}
|
||||
}
|
||||
throw new Error(
|
||||
`Can't convert "${value}" to any of: ${(t.types || []).map(typeToString).join(", ")}`,
|
||||
)
|
||||
case "stringLiteral":
|
||||
case "numberLiteral":
|
||||
case "booleanLiteral":
|
||||
if (value === t.value) return value
|
||||
throw new Error(
|
||||
`Argument does not match expected value (${t.value}): ${value}`,
|
||||
)
|
||||
case "reflection": {
|
||||
const decl = t.declaration || {}
|
||||
if (decl.signatures?.length) {
|
||||
throw new Error(
|
||||
`Conversion to function not implemented: ${value}`,
|
||||
)
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
throw new Error(`Can't convert to object: ${value}`)
|
||||
}
|
||||
}
|
||||
case "reference":
|
||||
throw new Error(
|
||||
"Conversion of simple type references not implemented.",
|
||||
)
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/** Walk `path` through an object-type reflection, coercing `value` against the matched leaf. */
|
||||
export function convertMember(
|
||||
t: Node | undefined,
|
||||
path: string[],
|
||||
value: any,
|
||||
): any {
|
||||
const decl = t?.type === "reflection" ? t.declaration : undefined
|
||||
const named: Record<string, Node> = {}
|
||||
let indexSig: Node | undefined
|
||||
for (const ch of decl?.children || []) {
|
||||
if (ch.name && ch.type) named[ch.name] = ch.type
|
||||
}
|
||||
const idx = decl?.indexSignature
|
||||
const idxArr = Array.isArray(idx) ? idx : idx ? [idx] : []
|
||||
for (const sig of idxArr) if (sig?.type) indexSig = sig.type
|
||||
|
||||
const sub = named[path[0]] ?? indexSig
|
||||
if (!sub) return value
|
||||
if (typeKind(sub) === "object")
|
||||
return convertMember(sub, path.slice(1), value)
|
||||
return convert(sub, value)
|
||||
}
|
||||
|
|
@ -1,26 +1,20 @@
|
|||
/** Ex Mode (AKA cmd mode) */
|
||||
|
||||
import { FunctionType } from "../../compiler/types/AllTypes"
|
||||
import { everything as metadata } from "@src/.metadata.generated"
|
||||
import { excmdsFunctions, paramTypes, convert } from "@src/.metadata.generated"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
import * as Logging from "@src/lib/logging"
|
||||
|
||||
const logger = new Logging.Logger("exmode")
|
||||
|
||||
function convertArgs(types, argv) {
|
||||
function convertArgs(params, argv) {
|
||||
const typedArgs = []
|
||||
for (
|
||||
let itypes = 0, iargv = 0;
|
||||
itypes < types.length && iargv < argv.length;
|
||||
++itypes && ++iargv
|
||||
) {
|
||||
const curType = types[itypes]
|
||||
const curArg = argv[iargv]
|
||||
for (let i = 0, j = 0; i < params.length && j < argv.length; ++i && ++j) {
|
||||
const p = params[i]
|
||||
// Special casing arrays because that's why the previous arg conversion code did
|
||||
if (curType.isDotDotDot || curType.kind === "array") {
|
||||
return typedArgs.concat(curType.convert(argv.slice(iargv)))
|
||||
if (p.flags?.isRest || p.type?.type === "array") {
|
||||
return typedArgs.concat(convert(p.type, argv.slice(j)))
|
||||
}
|
||||
typedArgs.push(curType.convert(curArg))
|
||||
typedArgs.push(convert(p.type, argv[j]))
|
||||
}
|
||||
return typedArgs
|
||||
}
|
||||
|
|
@ -47,18 +41,13 @@ export function parser(exstr: string, all_excmds: any): any[] {
|
|||
// Convert arguments, but only for ex commands
|
||||
let converted_args
|
||||
if (namespce == "" && args.length > 0) {
|
||||
let types
|
||||
try {
|
||||
types = (metadata.getFile("src/excmds.ts").getFunction(funcName)
|
||||
.type as FunctionType).args
|
||||
} catch (e) {
|
||||
const fn = excmdsFunctions[funcName]
|
||||
if (!fn) {
|
||||
// user defined functions?
|
||||
types = null
|
||||
converted_args = args
|
||||
}
|
||||
if (types !== null) {
|
||||
} else {
|
||||
try {
|
||||
converted_args = convertArgs(types, args)
|
||||
converted_args = convertArgs(paramTypes(fn), args)
|
||||
} catch (e) {
|
||||
logger.error("Error executing or parsing:", exstr, e)
|
||||
throw e
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"moduleResolution": "node",
|
||||
"module": "es2020",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"noImplicitAny": false,
|
||||
"noEmitOnError": true,
|
||||
"outDir": "build/tsc-out",
|
||||
|
|
|
|||
Loading…
Reference in a new issue