mirror of
https://github.com/retorquere/zotero-better-bibtex.git
synced 2022-05-23 09:36:29 +03:00
lintint
This commit is contained in:
2
babel
2
babel
Submodule babel updated: 187ac9b061...8d22061b5a
143
setup/api-extractor.ts
Executable file
143
setup/api-extractor.ts
Executable file
@@ -0,0 +1,143 @@
|
||||
import * as ts from 'typescript'
|
||||
import * as fs from 'fs'
|
||||
|
||||
export type Parameter = {
|
||||
name: string
|
||||
default: string | number | boolean
|
||||
}
|
||||
export type Method = {
|
||||
doc: string
|
||||
parameters: Parameter[]
|
||||
schema: any
|
||||
}
|
||||
|
||||
export class API {
|
||||
private ast: ts.SourceFile
|
||||
public classes: Record<string, Record<string, Method>> = {}
|
||||
|
||||
constructor(filename: string) {
|
||||
this.ast = ts.createSourceFile(filename, fs.readFileSync(filename, 'utf8'), ts.ScriptTarget.Latest)
|
||||
this.ast.forEachChild(stmt => {
|
||||
if (ts.isClassDeclaration(stmt)) {
|
||||
this.ClassDeclaration(stmt)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private ClassDeclaration(cls: ts.ClassDeclaration): void {
|
||||
const className: string = cls.name.getText(this.ast)
|
||||
if (!className) return
|
||||
|
||||
cls.forEachChild(member => {
|
||||
if (ts.isMethodDeclaration(member)) this.MethodDeclaration(className, member)
|
||||
})
|
||||
}
|
||||
|
||||
private MethodDeclaration(className: string, method: ts.MethodDeclaration): void {
|
||||
const methodName: string = method.name.getText(this.ast)
|
||||
if (!methodName) return
|
||||
|
||||
const comment_ranges = ts.getLeadingCommentRanges(this.ast.getFullText(), method.getFullStart())
|
||||
if (!comment_ranges) return
|
||||
let comment = this.ast.getFullText().slice(comment_ranges[0].pos, comment_ranges[0].end)
|
||||
if (!comment.startsWith('/**')) return
|
||||
comment = comment.replace(/^\/\*\*/, '').replace(/\*\/$/, '').trim().split('\n').map(line => line.replace(/^\s*[*]\s*/, '')).join('\n').replace(/\n+/g, newlines => newlines.length > 1 ? '\n\n' : ' ')
|
||||
|
||||
if (!this.classes[className]) this.classes[className] = {}
|
||||
|
||||
this.classes[className][methodName] = {
|
||||
doc: comment,
|
||||
parameters: [],
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
required: [],
|
||||
},
|
||||
}
|
||||
|
||||
method.forEachChild(param => {
|
||||
if (ts.isParameter(param)) this.ParameterDeclaration(this.classes[className][methodName], param)
|
||||
})
|
||||
}
|
||||
|
||||
private ParameterDeclaration(method: Method, param: ts.ParameterDeclaration) {
|
||||
const p: Parameter = {
|
||||
name: param.name.getText(this.ast),
|
||||
default: this.initializer(param.initializer),
|
||||
}
|
||||
method.parameters.push(p)
|
||||
|
||||
method.schema.properties[p.name] = param.type ? this.schema(param.type) : { type: typeof p.default }
|
||||
if (!param.initializer && !param.questionToken) method.schema.required.push(p.name)
|
||||
}
|
||||
|
||||
private initializer(init): string | number {
|
||||
if (!init) return undefined
|
||||
|
||||
switch (init.kind) {
|
||||
case ts.SyntaxKind.StringLiteral:
|
||||
return init.text as string
|
||||
|
||||
case ts.SyntaxKind.NumericLiteral:
|
||||
case ts.SyntaxKind.FirstLiteralToken: // https://github.com/microsoft/TypeScript/issues/18062
|
||||
return parseFloat(init.getText(this.ast))
|
||||
|
||||
default:
|
||||
throw new Error(`Unexpected kind ${init.kind} ${ts.SyntaxKind[init.kind]} of initializer ${JSON.stringify(init)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private schema(type: ts.TypeNode): any {
|
||||
switch (type.kind) {
|
||||
case ts.SyntaxKind.UnionType:
|
||||
return this.UnionType(type as unknown as ts.UnionType)
|
||||
|
||||
case ts.SyntaxKind.LiteralType:
|
||||
return this.LiteralType(type as unknown as ts.LiteralTypeNode)
|
||||
|
||||
case ts.SyntaxKind.StringKeyword:
|
||||
return { type: 'string' }
|
||||
|
||||
case ts.SyntaxKind.BooleanKeyword:
|
||||
return { type: 'boolean' }
|
||||
|
||||
// case ts.SyntaxKind.TypeReference:
|
||||
// return null
|
||||
|
||||
case ts.SyntaxKind.NumberKeyword:
|
||||
return { type: 'number' }
|
||||
|
||||
default:
|
||||
throw {...type, kindName: ts.SyntaxKind[type.kind] } // eslint-disable-line no-throw-literal
|
||||
}
|
||||
}
|
||||
|
||||
private LiteralType(type: ts.LiteralTypeNode): any {
|
||||
let value: string = type.literal.getText(this.ast)
|
||||
if (ts.isStringLiteral(type.literal)) value = JSON.parse(value)
|
||||
|
||||
return { const: value }
|
||||
}
|
||||
|
||||
private UnionType(type: ts.UnionType): any {
|
||||
const types = type.types.map((t: ts.Type) => this.schema(t as unknown as ts.TypeNode)) // eslint-disable-line @typescript-eslint/no-unsafe-return
|
||||
|
||||
if (types.length === 1) return types[0]
|
||||
|
||||
const consts = []
|
||||
const other = types.filter(t => {
|
||||
if (typeof t.const === 'undefined') return true
|
||||
consts.push(t.const)
|
||||
return false
|
||||
})
|
||||
|
||||
switch (consts.length) {
|
||||
case 0:
|
||||
case 1:
|
||||
return { oneOf: types }
|
||||
default:
|
||||
return { oneOf : other.concat({ enum: consts }) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,63 @@
|
||||
#!/usr/bin/env npx ts-node
|
||||
/* eslint-disable prefer-template, @typescript-eslint/no-unsafe-return */
|
||||
|
||||
import * as ts from 'typescript'
|
||||
import { Method, API } from './api-extractor'
|
||||
import * as fs from 'fs'
|
||||
const stringify = require('safe-stable-stringify')
|
||||
import stringify from 'safe-stable-stringify'
|
||||
|
||||
const filename = 'content/key-manager/formatter.ts'
|
||||
const ast = ts.createSourceFile(filename, fs.readFileSync(filename, 'utf8'), ts.ScriptTarget.Latest)
|
||||
|
||||
function kindName(node) {
|
||||
node.kindName = ts.SyntaxKind[node.kind]
|
||||
node.forEachChild(kindName)
|
||||
}
|
||||
kindName(ast)
|
||||
|
||||
function assert(cond, msg) {
|
||||
if (!cond) throw new Error(msg)
|
||||
}
|
||||
|
||||
const Method = new class {
|
||||
class FormatterAPI {
|
||||
private formatter: Record<string, Method>
|
||||
public signature: Record<string, any> = {}
|
||||
public doc: { function: Record<string, any>, filter: Record<string, any> } = { function: {}, filter: {} }
|
||||
public doc: { function: Record<string, string>, filter: Record<string, string> } = { function: {}, filter: {} }
|
||||
|
||||
const2enum(types) {
|
||||
const consts = []
|
||||
const other = types.filter(type => {
|
||||
if (typeof type.const === 'undefined') return true
|
||||
consts.push(type.const)
|
||||
return false
|
||||
})
|
||||
constructor(source: string) {
|
||||
this.formatter = new API(source).classes.PatternFormatter
|
||||
for (const [name, method] of Object.entries(this.formatter)) {
|
||||
const kind = {$: 'function', _: 'filter'}[name[0]]
|
||||
if (!kind) continue
|
||||
|
||||
switch (consts.length) {
|
||||
case 0:
|
||||
case 1:
|
||||
return types
|
||||
default:
|
||||
return other.concat({ enum: consts })
|
||||
}
|
||||
}
|
||||
this.signature[name] = JSON.parse(JSON.stringify({
|
||||
parameters: method.parameters.map(p => p.name),
|
||||
schema: method.schema,
|
||||
}))
|
||||
|
||||
types(node) {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.UnionType:
|
||||
return { oneOf: this.const2enum(node.types.map(t => this.types(t)).filter(type => type)) }
|
||||
|
||||
case ts.SyntaxKind.LiteralType:
|
||||
return { const: node.literal.text }
|
||||
|
||||
case ts.SyntaxKind.StringKeyword:
|
||||
return { type: 'string' }
|
||||
|
||||
case ts.SyntaxKind.BooleanKeyword:
|
||||
return { type: 'boolean' }
|
||||
|
||||
case ts.SyntaxKind.TypeReference:
|
||||
return null
|
||||
|
||||
case ts.SyntaxKind.NumberKeyword:
|
||||
return { type: 'number' }
|
||||
|
||||
default:
|
||||
throw {...node, kindName: ts.SyntaxKind[node.kind] } // eslint-disable-line no-throw-literal
|
||||
}
|
||||
}
|
||||
|
||||
type(node) {
|
||||
const types = this.types(node)
|
||||
|
||||
if (types.oneOf) {
|
||||
assert(types.oneOf.length, types)
|
||||
if (types.oneOf.length === 1) {
|
||||
return types.oneOf[0]
|
||||
let names = [ name.substr(1) ]
|
||||
let name_edtr = ''
|
||||
if (kind === 'function' && method.parameters.find(p => p.name === 'onlyEditors')) { // auth function
|
||||
for (const [author, editor] of [['authors', 'editors'], ['auth.auth', 'edtr.edtr'], [ 'auth', 'edtr' ]]) {
|
||||
if (names[0].startsWith(author)) {
|
||||
names.push(name_edtr = names[0].replace(author, editor))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (name_edtr) {
|
||||
this.signature[name_edtr] = JSON.parse(JSON.stringify(this.signature[name]))
|
||||
|
||||
return types
|
||||
for (const mname of [name, name_edtr]) {
|
||||
this.signature[mname].schema.properties.onlyEditors = { const: mname === name_edtr }
|
||||
}
|
||||
}
|
||||
|
||||
names = names.map(n => n.replace(/__/g, '.').replace(/_/g, '-'))
|
||||
if (kind === 'function') {
|
||||
if (method.parameters.find(p => p.name === 'n')) names = names.map(n => `${n}N`)
|
||||
if (method.parameters.find(p => p.name === 'm')) names = names.map(n => `${n}_M`)
|
||||
}
|
||||
let quoted = names.map(n => '`' + n + '`').join(' / ')
|
||||
|
||||
switch (kind) {
|
||||
case 'function':
|
||||
if (method.parameters.find(p => p.name === 'withInitials')) quoted += ', `+initials`'
|
||||
if (method.parameters.find(p => p.name === 'joiner')) quoted += ', `+<joinchar>`'
|
||||
break
|
||||
case 'filter':
|
||||
if (method.parameters.length) quoted += '=' + method.parameters.map(p => `${p.name}${method.schema.required.includes(p.name) ? '' : '?'} (${this.typedoc(method.schema.properties[p.name])})`).join(', ')
|
||||
break
|
||||
}
|
||||
|
||||
this.doc[kind][quoted] = method.doc
|
||||
}
|
||||
}
|
||||
|
||||
private typedoc(type): string {
|
||||
@@ -84,115 +67,10 @@ const Method = new class {
|
||||
if (type.enum) return type.enum.map(t => this.typedoc({ const: t })).join(' | ')
|
||||
throw new Error(`no rule for ${JSON.stringify(type)}`)
|
||||
}
|
||||
|
||||
initializer(init) {
|
||||
if (!init) return undefined
|
||||
switch (init.kind) {
|
||||
case ts.SyntaxKind.StringLiteral:
|
||||
return init.text
|
||||
|
||||
case ts.SyntaxKind.NumericLiteral:
|
||||
case ts.SyntaxKind.FirstLiteralToken: // https://github.com/microsoft/TypeScript/issues/18062
|
||||
assert(!isNaN(parseFloat(init.text)), `${init.text} is not a number`)
|
||||
return parseFloat(init.text)
|
||||
|
||||
default:
|
||||
throw new Error(`Unexpected type ${init.type} of initializer ${JSON.stringify(init)}`)
|
||||
}
|
||||
}
|
||||
|
||||
add(method: ts.MethodDeclaration) {
|
||||
const method_name: string = method.name.kind === ts.SyntaxKind.Identifier ? method.name.escapedText as string : ''
|
||||
assert(method_name, method.name.getText(ast))
|
||||
if (!method_name.match(/^[$_]/)) return
|
||||
let method_name_edtr = ''
|
||||
|
||||
assert(!this.signature[method_name], `${method_name} already exists`)
|
||||
const params = method.parameters.map(p => ({
|
||||
name: p.name.kind === ts.SyntaxKind.Identifier ? (p.name.escapedText as string) : '',
|
||||
type: p.type? this.type(p.type) : { type: typeof this.initializer(p.initializer) },
|
||||
optional: !!(p.initializer || p.questionToken),
|
||||
default: this.initializer(p.initializer),
|
||||
}))
|
||||
const kind = {$: 'function', _: 'filter'}[method_name[0]]
|
||||
let names = [ method_name.substr(1) ]
|
||||
if (params.find(p => p.name === 'onlyEditors')) {
|
||||
for (const [author, editor] of [['authors', 'editors'], ['auth.auth', 'edtr.edtr'], [ 'auth', 'edtr' ]]) {
|
||||
if (names[0].startsWith(author)) {
|
||||
names.push(names[0].replace(author, editor))
|
||||
method_name_edtr = editor
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
names = names.map(n => n.replace(/__/g, '.').replace(/_/g, '-'))
|
||||
if (kind === 'function') {
|
||||
if (params.find(p => p.name === 'n')) names = names.map(n => `${n}N`)
|
||||
if (params.find(p => p.name === 'm')) names = names.map(n => `${n}_M`)
|
||||
}
|
||||
let quoted = names.map(n => '`' + n + '`').join(' / ')
|
||||
switch (kind) {
|
||||
case 'function':
|
||||
if (params.find(p => p.name === 'withInitials')) quoted += ', `+initials`'
|
||||
if (params.find(p => p.name === 'joiner')) quoted += ', `+<joinchar>`'
|
||||
break
|
||||
case 'filter':
|
||||
if (params.length) quoted += '=' + params.map(p => `${p.name}${p.optional ? '?' : ''} (${this.typedoc(p.type)})`).join(', ')
|
||||
break
|
||||
}
|
||||
|
||||
const comment_ranges = ts.getLeadingCommentRanges(ast.getFullText(), method.getFullStart())
|
||||
assert(comment_ranges, `${method_name} has no documentation`)
|
||||
let comment = ast.getFullText().slice(comment_ranges[0].pos, comment_ranges[0].end)
|
||||
assert(comment.startsWith('/**'), `comment for ${method_name} does not start with a doc-comment indicator`)
|
||||
comment = comment.replace(/^\/\*\*/, '').replace(/\*\/$/, '').trim().split('\n').map(line => line.replace(/^\s*[*]\s*/, '')).join('\n').replace(/\n+/g, newlines => newlines.length > 1 ? '\n\n' : ' ')
|
||||
this.doc[kind][quoted] = comment
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
required: [],
|
||||
}
|
||||
names = []
|
||||
for (const p of params) {
|
||||
names.push(p.name)
|
||||
if (!p.optional) schema.required.push(p.name)
|
||||
schema.properties[p.name] = p.type
|
||||
}
|
||||
if (!schema.required.length) delete schema.required
|
||||
|
||||
this.signature[method_name] = JSON.parse(JSON.stringify({
|
||||
arguments: names,
|
||||
schema,
|
||||
}))
|
||||
if (method_name_edtr) {
|
||||
this.signature[method_name_edtr] = JSON.parse(JSON.stringify({
|
||||
arguments: names,
|
||||
schema,
|
||||
}))
|
||||
|
||||
for (const mname of [method_name, method_name_edtr]) {
|
||||
this.signature[mname].schema.properties.onlyEditors = { const: mname === method_name_edtr }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ast.forEachChild((node: ts.Node) => {
|
||||
// process only classes
|
||||
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
|
||||
const api = new FormatterAPI('content/key-manager/formatter.ts')
|
||||
|
||||
// get feautures of ClassDeclarations
|
||||
const cls: ts.ClassDeclaration = node as ts.ClassDeclaration
|
||||
|
||||
// process class childs
|
||||
cls.forEachChild((method: ts.Node) => {
|
||||
if (method.kind === ts.SyntaxKind.MethodDeclaration) Method.add(method as ts.MethodDeclaration)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
fs.writeFileSync('gen/key-formatter-methods.json', JSON.stringify(Method.signature, null, 2))
|
||||
fs.writeFileSync('site/data/citekeyformatters/functions.json', stringify(Method.doc.function, null, 2))
|
||||
fs.writeFileSync('site/data/citekeyformatters/filters.json', stringify(Method.doc.filter, null, 2))
|
||||
fs.writeFileSync('gen/key-formatter-methods.json', JSON.stringify(api.signature, null, 2))
|
||||
fs.writeFileSync('site/data/citekeyformatters/functions.json', stringify(api.doc.function, null, 2))
|
||||
fs.writeFileSync('site/data/citekeyformatters/filters.json', stringify(api.doc.filter, null, 2))
|
||||
|
||||
Submodule site/themes/learn updated: d198cbe65f...e817f53d69
Reference in New Issue
Block a user